diff --git a/Core/build.xml b/Core/build.xml index d246edaa54..2c461a5017 100644 --- a/Core/build.xml +++ b/Core/build.xml @@ -103,6 +103,11 @@ + + + + + diff --git a/Core/ivy.xml b/Core/ivy.xml index 1ed71d69cf..c60161dd90 100644 --- a/Core/ivy.xml +++ b/Core/ivy.xml @@ -7,12 +7,8 @@ - - - - @@ -27,6 +23,7 @@ + @@ -34,5 +31,7 @@ + + diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties index 90ce31dab9..335ff4fbc3 100644 --- a/Core/nbproject/project.properties +++ b/Core/nbproject/project.properties @@ -40,6 +40,7 @@ file.reference.xmpcore-5.1.3.jar=release/modules/ext/xmpcore-5.1.3.jar file.reference.xz-1.6.jar=release/modules/ext/xz-1.6.jar file.reference.zookeeper-3.4.6.jar=release/modules/ext/zookeeper-3.4.6.jar file.reference.SparseBitSet-1.1.jar=release/modules/ext/SparseBitSet-1.1.jar +file.reference.commons-validator-1.6.jar=release/modules/ext/commons-validator-1.6.jar javac.source=1.8 javac.compilerargs=-Xlint -Xlint:-serial license.file=../LICENSE-2.0.txt diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml index 303d14422c..50ffa2b436 100644 --- a/Core/nbproject/project.xml +++ b/Core/nbproject/project.xml @@ -329,6 +329,7 @@ org.sleuthkit.autopsy.guiutils org.sleuthkit.autopsy.healthmonitor org.sleuthkit.autopsy.ingest + org.sleuthkit.autopsy.ingest.events org.sleuthkit.autopsy.keywordsearchservice org.sleuthkit.autopsy.menuactions org.sleuthkit.autopsy.modules.encryptiondetection @@ -356,6 +357,10 @@ ext/cxf-rt-transports-http-3.0.16.jar release/modules/ext/cxf-rt-transports-http-3.0.16.jar + + ext/commons-validator-1.6.jar + release/modules/ext/commons-validator-1.6.jar + ext/curator-framework-2.8.0.jar release/modules/ext/curator-framework-2.8.0.jar diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java index 9019ae162d..dcb2af3eef 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java @@ -71,12 +71,13 @@ import org.sleuthkit.autopsy.casemodule.events.AddingDataSourceEvent; import org.sleuthkit.autopsy.casemodule.events.AddingDataSourceFailedEvent; import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagDeletedEvent; +import org.sleuthkit.autopsy.casemodule.events.CommentChangedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.casemodule.events.DataSourceAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ReportAddedEvent; import org.sleuthkit.autopsy.casemodule.services.Services; -import org.sleuthkit.autopsy.commonfilesearch.CommonFilesSearchAction; +import org.sleuthkit.autopsy.commonfilesearch.CommonAttributeSearchAction; import org.sleuthkit.autopsy.communications.OpenCommVisualizationToolAction; import org.sleuthkit.autopsy.coordinationservice.CoordinationService; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CategoryNode; @@ -372,7 +373,12 @@ public class Case { * old value of the PropertyChangeEvent is the display name of the tag * definition that has changed. */ - TAG_DEFINITION_CHANGED; + TAG_DEFINITION_CHANGED, + /** + * An item in the central repository has had its comment modified. The + * old value is null, the new value is string for current comment. + */ + CR_COMMENT_CHANGED; }; @@ -1087,7 +1093,7 @@ public class Case { CallableSystemAction.get(CaseDeleteAction.class).setEnabled(true); CallableSystemAction.get(OpenTimelineAction.class).setEnabled(true); CallableSystemAction.get(OpenCommVisualizationToolAction.class).setEnabled(true); - CallableSystemAction.get(CommonFilesSearchAction.class).setEnabled(true); + CallableSystemAction.get(CommonAttributeSearchAction.class).setEnabled(true); CallableSystemAction.get(OpenOutputFolderAction.class).setEnabled(false); /* @@ -1100,7 +1106,7 @@ public class Case { /* * Open the top components (windows within the main application * window). - * + * * Note: If the core windows are not opened here, they will be * opened via the DirectoryTreeTopComponent 'propertyChange()' * method on a DATA_SOURCE_ADDED event. @@ -1141,7 +1147,7 @@ public class Case { CallableSystemAction.get(OpenTimelineAction.class).setEnabled(false); CallableSystemAction.get(OpenCommVisualizationToolAction.class).setEnabled(false); CallableSystemAction.get(OpenOutputFolderAction.class).setEnabled(false); - CallableSystemAction.get(CommonFilesSearchAction.class).setEnabled(false); + CallableSystemAction.get(CommonAttributeSearchAction.class).setEnabled(false); /* * Clear the notifications in the notfier component in the lower @@ -1533,6 +1539,22 @@ public class Case { eventPublisher.publish(new AutopsyEvent(Events.TAG_DEFINITION_CHANGED.toString(), changedTagName, null)); } + /** + * Notifies case event subscribers that a central repository comment has been changed. + * + * This should not be called from the event dispatch thread (EDT) + * + * @param contentId the objectId for the Content which has had its central repo comment changed + * @param newComment the new value of the comment + */ + public void notifyCentralRepoCommentChanged(long contentId, String newComment) { + try { + eventPublisher.publish(new CommentChangedEvent(contentId, newComment)); + } catch (NoCurrentCaseException ex) { + logger.log(Level.WARNING, "Unable to send notifcation regarding comment change due to no current case being open", ex); + } + } + /** * Notifies case event subscribers that an artifact tag has been added. * diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java index c7fa2c3de3..8d6dcffdd9 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java @@ -197,7 +197,7 @@ public class LocalFilesDSProcessor implements DataSourceProcessor, AutoIngestDat command.add("-f"); command.add("files"); command.add("-t"); - File l01Dir = new File(Case.getCurrentCaseThrows().getModuleDirectory(), L01_EXTRACTION_DIR); //WJS-TODO change to getOpenCase() when that method exists + File l01Dir = new File(Case.getCurrentCaseThrows().getModuleDirectory(), L01_EXTRACTION_DIR); if (!l01Dir.exists()) { l01Dir.mkdirs(); } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/events/CommentChangedEvent.java b/Core/src/org/sleuthkit/autopsy/casemodule/events/CommentChangedEvent.java new file mode 100644 index 0000000000..8cfb8ace0d --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/events/CommentChangedEvent.java @@ -0,0 +1,60 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.casemodule.events; + +import java.io.Serializable; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.events.AutopsyEvent; + +/** + * Event published when a central repsoitory comment is changed + */ +public class CommentChangedEvent extends AutopsyEvent implements Serializable { + + private static final long serialVersionUID = 1L; + + private final long contentID; + private final String caseName; //included so we can eventually use this event between cases + + /** + * Constructs a CommentChangedEvent which is published when a central + * repository comment is changed. + * + * @param contentId the objectId of the Content which has had its central repository comment changed + * @param newComment the new value of the comment + * @throws org.sleuthkit.autopsy.casemodule.NoCurrentCaseException if there is no current case open when this event is sent + */ + public CommentChangedEvent(long contentId, String newComment) throws NoCurrentCaseException { + super(Case.Events.CR_COMMENT_CHANGED.toString(), null, newComment); + contentID = contentId; + //get the caseName as well so that this event could be used for notifications between cases without method signature change + caseName = Case.getCurrentCaseThrows().getName(); + + } + + /** + * Get the object id of the content which this event is associated with. + * + * @return the objectId of the content this event is associated with + */ + public long getContentID() { + return contentID; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/AddEditCentralRepoCommentAction.java b/Core/src/org/sleuthkit/autopsy/centralrepository/AddEditCentralRepoCommentAction.java index bdbf6a60b0..0698992fa7 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/AddEditCentralRepoCommentAction.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/AddEditCentralRepoCommentAction.java @@ -24,6 +24,8 @@ import javax.swing.AbstractAction; import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor; import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.centralrepository.datamodel.EamArtifactUtil; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; @@ -35,38 +37,35 @@ import org.sleuthkit.datamodel.AbstractFile; * An AbstractAction to manage adding and modifying a Central Repository file * instance comment. */ + + @Messages({"AddEditCentralRepoCommentAction.menuItemText.addEditCentralRepoComment=Add/Edit Central Repository Comment"}) public final class AddEditCentralRepoCommentAction extends AbstractAction { private static final Logger logger = Logger.getLogger(AddEditCentralRepoCommentAction.class.getName()); + private static final long serialVersionUID = 1L; private boolean addToDatabase; private CorrelationAttributeInstance correlationAttributeInstance; private String comment; - - /** - * Constructor to create an instance given a CorrelationAttribute. - * - * @param correlationAttribute The correlation attribute to modify. - */ - public AddEditCentralRepoCommentAction(CorrelationAttributeInstance correlationAttribute) { - super(Bundle.AddEditCentralRepoCommentAction_menuItemText_addEditCentralRepoComment()); - this.correlationAttributeInstance = correlationAttribute; - } + private final Long fileId; /** * Constructor to create an instance given an AbstractFile. * * @param file The file from which a correlation attribute to modify is * derived. + * */ public AddEditCentralRepoCommentAction(AbstractFile file) { super(Bundle.AddEditCentralRepoCommentAction_menuItemText_addEditCentralRepoComment()); + fileId = file.getId(); correlationAttributeInstance = EamArtifactUtil.getInstanceFromContent(file); if (correlationAttributeInstance == null) { addToDatabase = true; correlationAttributeInstance = EamArtifactUtil.makeInstanceFromContent(file); } + } /** @@ -100,6 +99,11 @@ public final class AddEditCentralRepoCommentAction extends AbstractAction { } comment = centralRepoCommentDialog.getComment(); + try { + Case.getCurrentCaseThrows().notifyCentralRepoCommentChanged(fileId, comment); + } catch (NoCurrentCaseException ex) { + logger.log(Level.WARNING, "Case not open after changing central repository comment", ex); + } } catch (EamDbException ex) { logger.log(Level.SEVERE, "Error adding comment", ex); NotifyDescriptor notifyDescriptor = new NotifyDescriptor.Message( @@ -120,10 +124,10 @@ public final class AddEditCentralRepoCommentAction extends AbstractAction { public String getComment() { return comment; } - + /** * Retrieve the associated correlation attribute. - * + * * @return The correlation attribute. */ public CorrelationAttributeInstance getCorrelationAttribute() { diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties index acb3fa42d1..2a9cd7b456 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties @@ -3,7 +3,6 @@ DataContentViewerOtherCases.showCaseDetailsMenuItem.text=Show Case Details DataContentViewerOtherCases.table.toolTip.text=Click column name to sort. Right-click on the table for more options. DataContentViewerOtherCases.exportToCSVMenuItem.text=Export Selected Rows to CSV DataContentViewerOtherCases.showCommonalityMenuItem.text=Show Frequency -DataContentViewerOtherCases.addCommentMenuItem.text=Add/Edit Central Repository Comment DataContentViewerOtherCases.earliestCaseDate.text=Earliest Case Date DataContentViewerOtherCases.earliestCaseLabel.toolTipText= DataContentViewerOtherCases.earliestCaseLabel.text=Central Repository Starting Date: diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form index 9c42be16a8..1f1e497aa7 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form @@ -39,13 +39,6 @@ - - - - - - - @@ -80,7 +73,7 @@ - + @@ -106,7 +99,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java index 9da0b577b3..c6301bfb89 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java @@ -56,7 +56,7 @@ import org.openide.util.NbBundle.Messages; import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import org.sleuthkit.autopsy.centralrepository.AddEditCentralRepoCommentAction; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException; import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.centralrepository.datamodel.EamArtifactUtil; @@ -79,14 +79,14 @@ import org.sleuthkit.datamodel.TskData; * View correlation results from other cases */ @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives -@ServiceProvider(service = DataContentViewer.class, position = 8) +@ServiceProvider(service = DataContentViewer.class, position = 9) @Messages({"DataContentViewerOtherCases.title=Other Occurrences", "DataContentViewerOtherCases.toolTip=Displays instances of the selected file/artifact from other occurrences.",}) public class DataContentViewerOtherCases extends JPanel implements DataContentViewer { private static final long serialVersionUID = -1L; - private static final Logger logger = Logger.getLogger(DataContentViewerOtherCases.class.getName()); + private static final Logger LOGGER = Logger.getLogger(DataContentViewerOtherCases.class.getName()); private static final int DEFAULT_MIN_CELL_WIDTH = 15; private static final int CELL_TEXT_WIDTH_PADDING = 5; @@ -123,24 +123,11 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi try { saveToCSV(); } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS + LOGGER.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS } } else if (jmi.equals(showCommonalityMenuItem)) { showCommonalityDetails(); - } else if (jmi.equals(addCommentMenuItem)) { - try { - OtherOccurrenceNodeInstanceData selectedNode = (OtherOccurrenceNodeInstanceData) tableModel.getRow(otherCasesTable.getSelectedRow()); - AddEditCentralRepoCommentAction action = new AddEditCentralRepoCommentAction(selectedNode.getCorrelationAttribute()); - action.actionPerformed(null); - String currentComment = action.getComment(); - if (currentComment != null) { - selectedNode.updateComment(action.getComment()); - otherCasesTable.repaint(); - } - } catch (EamDbException ex) { - logger.log(Level.SEVERE, "Error performing Add/Edit Central Repository Comment action", ex); - } - } + } } }; @@ -148,7 +135,6 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi selectAllMenuItem.addActionListener(actList); showCaseDetailsMenuItem.addActionListener(actList); showCommonalityMenuItem.addActionListener(actList); - addCommentMenuItem.addActionListener(actList); // Set background of every nth row as light grey. TableCellRenderer renderer = new DataContentViewerOtherCasesTableCellRenderer(); @@ -179,17 +165,21 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi try { EamDb dbManager = EamDb.getInstance(); for (CorrelationAttributeInstance eamArtifact : correlationAttributes) { - percentage = dbManager.getFrequencyPercentage(eamArtifact); - msg.append(Bundle.DataContentViewerOtherCases_correlatedArtifacts_byType(percentage, - eamArtifact.getCorrelationType().getDisplayName(), - eamArtifact.getCorrelationValue())); + try { + percentage = dbManager.getFrequencyPercentage(eamArtifact); + msg.append(Bundle.DataContentViewerOtherCases_correlatedArtifacts_byType(percentage, + eamArtifact.getCorrelationType().getDisplayName(), + eamArtifact.getCorrelationValue())); + } catch (CorrelationAttributeNormalizationException ex) { + LOGGER.log(Level.WARNING, String.format("Error getting commonality details for artifact with ID: %s.", eamArtifact.getID()), ex); + } } JOptionPane.showConfirmDialog(showCommonalityMenuItem, msg.toString(), Bundle.DataContentViewerOtherCases_correlatedArtifacts_title(), DEFAULT_OPTION, PLAIN_MESSAGE); } catch (EamDbException ex) { - logger.log(Level.SEVERE, "Error getting commonality details.", ex); + LOGGER.log(Level.SEVERE, "Error getting commonality details.", ex); JOptionPane.showConfirmDialog(showCommonalityMenuItem, Bundle.DataContentViewerOtherCases_correlatedArtifacts_failed(), Bundle.DataContentViewerOtherCases_correlatedArtifacts_title(), @@ -242,7 +232,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi DEFAULT_OPTION, PLAIN_MESSAGE); } } catch (EamDbException ex) { - logger.log(Level.SEVERE, "Error loading case details", ex); + LOGGER.log(Level.SEVERE, "Error loading case details", ex); JOptionPane.showConfirmDialog(showCaseDetailsMenuItem, Bundle.DataContentViewerOtherCases_caseDetailsDialog_noDetails(), caseDisplayName, @@ -305,7 +295,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi } } catch (IOException ex) { - logger.log(Level.SEVERE, "Error writing selected rows to CSV.", ex); + LOGGER.log(Level.SEVERE, "Error writing selected rows to CSV.", ex); } } @@ -397,7 +387,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi try { content = nodeBbArtifact.getSleuthkitCase().getContentById(nodeBbArtifact.getObjectID()); } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error retrieving blackboard artifact", ex); // NON-NLS + LOGGER.log(Level.SEVERE, "Error retrieving blackboard artifact", ex); // NON-NLS return null; } @@ -427,9 +417,9 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi if (bbArtifact != null && EamDb.isEnabled()) { ret.addAll(EamArtifactUtil.makeInstancesFromBlackboardArtifact(bbArtifact, false)); } - + // we can correlate based on the MD5 if it is enabled - if (this.file != null && EamDb.isEnabled()) { + if (this.file != null && EamDb.isEnabled()) { try { List artifactTypes = EamDb.getInstance().getDefinedCorrelationTypes(); @@ -438,33 +428,47 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi for (CorrelationAttributeInstance.Type aType : artifactTypes) { if (aType.getId() == CorrelationAttributeInstance.FILES_TYPE_ID) { CorrelationCase corCase = EamDb.getInstance().getCase(Case.getCurrentCase()); - ret.add(new CorrelationAttributeInstance( - md5, - aType, - corCase, - CorrelationDataSource.fromTSKDataSource(corCase, file.getDataSource()), - file.getParentPath() + file.getName(), - "", - file.getKnown())); + try { + ret.add(new CorrelationAttributeInstance( + md5, + aType, + corCase, + CorrelationDataSource.fromTSKDataSource(corCase, file.getDataSource()), + file.getParentPath() + file.getName(), + "", + file.getKnown())); + } catch (CorrelationAttributeNormalizationException ex) { + LOGGER.log(Level.INFO, String.format("Unable to check create CorrelationAttribtueInstance for value %s and type %s.", md5, aType.toString()), ex); + } break; } } } } catch (EamDbException | TskCoreException ex) { - logger.log(Level.SEVERE, "Error connecting to DB", ex); // NON-NLS - } + LOGGER.log(Level.SEVERE, "Error connecting to DB", ex); // NON-NLS + } } else { - try { - // If EamDb not enabled, get the Files default correlation type to allow Other Occurances to be enabled. - if (this.file != null) { - String md5 = this.file.getMd5Hash(); - if (md5 != null && !md5.isEmpty()) { - ret.add(new CorrelationAttributeInstance(CorrelationAttributeInstance.getDefaultCorrelationTypes().get(0), md5)); + + // If EamDb not enabled, get the Files default correlation type to allow Other Occurances to be enabled. + if (this.file != null) { + String md5 = this.file.getMd5Hash(); + if (md5 != null && !md5.isEmpty()) { + try { + final CorrelationAttributeInstance.Type fileAttributeType + = CorrelationAttributeInstance.getDefaultCorrelationTypes() + .stream() + .filter(attrType -> attrType.getId() == CorrelationAttributeInstance.FILES_TYPE_ID) + .findAny() + .get(); + + ret.add(new CorrelationAttributeInstance(fileAttributeType, md5)); + } catch (EamDbException ex) { + LOGGER.log(Level.SEVERE, "Error connecting to DB", ex); // NON-NLS + } catch (CorrelationAttributeNormalizationException ex) { + LOGGER.log(Level.INFO, String.format("Unable to create CorrelationAttributeInstance for value %s", md5), ex); // NON-NLS } } - } catch (EamDbException ex) { - logger.log(Level.SEVERE, "Error connecting to DB", ex); // NON-NLS } } @@ -496,9 +500,9 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi } } catch (EamDbException ex) { - logger.log(Level.SEVERE, "Error getting list of cases from database.", ex); // NON-NLS + LOGGER.log(Level.SEVERE, "Error getting list of cases from database.", ex); // NON-NLS } catch (ParseException ex) { - logger.log(Level.SEVERE, "Error parsing date of cases from database.", ex); // NON-NLS + LOGGER.log(Level.SEVERE, "Error parsing date of cases from database.", ex); // NON-NLS } } @@ -511,9 +515,9 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi * artifact. If the central repo is not enabled, this will only return files * from the current case with matching MD5 hashes. * - * @param corAttr CorrelationAttribute to query for + * @param corAttr CorrelationAttribute to query for * @param dataSourceName Data source to filter results - * @param deviceId Device Id to filter results + * @param deviceId Device Id to filter results * * @return A collection of correlated artifact instances */ @@ -558,13 +562,15 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi return nodeDataMap; } catch (EamDbException ex) { - logger.log(Level.SEVERE, "Error getting artifact instances from database.", ex); // NON-NLS + LOGGER.log(Level.SEVERE, "Error getting artifact instances from database.", ex); // NON-NLS + } catch (CorrelationAttributeNormalizationException ex) { + LOGGER.log(Level.INFO, "Error getting artifact instances from database.", ex); // NON-NLS } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS + LOGGER.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS } catch (TskCoreException ex) { // do nothing. // @@@ Review this behavior - logger.log(Level.SEVERE, "Exception while querying open case.", ex); // NON-NLS + LOGGER.log(Level.SEVERE, "Exception while querying open case.", ex); // NON-NLS } return new HashMap<>(0); @@ -574,7 +580,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi * Get all other abstract files in the current case with the same MD5 as the * selected node. * - * @param corAttr The CorrelationAttribute containing the MD5 to search for + * @param corAttr The CorrelationAttribute containing the MD5 to search for * @param openCase The current case * * @return List of matching AbstractFile objects @@ -708,7 +714,6 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi correlatedNodeDataMap.values().forEach((nodeData) -> { tableModel.addNodeData(nodeData); - }); } @@ -728,8 +733,8 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi * Adjust a given column for the text provided. * * @param columnIndex The index of the column to adjust. - * @param text The text whose length will be used to adjust the - * column width. + * @param text The text whose length will be used to adjust the column + * width. */ private void setColumnWidthToText(int columnIndex, String text) { TableColumn column = otherCasesTable.getColumnModel().getColumn(columnIndex); @@ -766,7 +771,6 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi exportToCSVMenuItem = new javax.swing.JMenuItem(); showCaseDetailsMenuItem = new javax.swing.JMenuItem(); showCommonalityMenuItem = new javax.swing.JMenuItem(); - addCommentMenuItem = new javax.swing.JMenuItem(); CSVFileChooser = new javax.swing.JFileChooser(); otherCasesPanel = new javax.swing.JPanel(); tableContainerPanel = new javax.swing.JPanel(); @@ -798,9 +802,6 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi org.openide.awt.Mnemonics.setLocalizedText(showCommonalityMenuItem, org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.showCommonalityMenuItem.text")); // NOI18N rightClickPopupMenu.add(showCommonalityMenuItem); - org.openide.awt.Mnemonics.setLocalizedText(addCommentMenuItem, org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.addCommentMenuItem.text")); // NOI18N - rightClickPopupMenu.add(addCommentMenuItem); - setMinimumSize(new java.awt.Dimension(1500, 10)); setOpaque(false); setPreferredSize(new java.awt.Dimension(1500, 44)); @@ -903,15 +904,12 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi enableCentralRepoActions = instanceData.isCentralRepoNode(); } } - - addCommentMenuItem.setVisible(enableCentralRepoActions); showCaseDetailsMenuItem.setVisible(enableCentralRepoActions); showCommonalityMenuItem.setVisible(enableCentralRepoActions); }//GEN-LAST:event_rightClickPopupMenuPopupMenuWillBecomeVisible // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JFileChooser CSVFileChooser; - private javax.swing.JMenuItem addCommentMenuItem; private javax.swing.JLabel earliestCaseDate; private javax.swing.JLabel earliestCaseLabel; private javax.swing.JMenuItem exportToCSVMenuItem; @@ -993,5 +991,4 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi return dataSourceID; } } - } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/AbstractSqlEamDb.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/AbstractSqlEamDb.java index 288363fe95..833c27ffcc 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/AbstractSqlEamDb.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/AbstractSqlEamDb.java @@ -652,7 +652,7 @@ abstract class AbstractSqlEamDb implements EamDb { preparedStatement.setString(1, eamArtifact.getCorrelationCase().getCaseUUID()); preparedStatement.setString(2, eamArtifact.getCorrelationDataSource().getDeviceID()); preparedStatement.setInt(3, eamArtifact.getCorrelationDataSource().getCaseID()); - preparedStatement.setString(4, eamArtifact.getCorrelationValue().toLowerCase()); + preparedStatement.setString(4, eamArtifact.getCorrelationValue()); preparedStatement.setString(5, eamArtifact.getFilePath().toLowerCase()); preparedStatement.setByte(6, eamArtifact.getKnownStatus().getFileKnownValue()); if ("".equals(eamArtifact.getComment())) { @@ -712,10 +712,10 @@ abstract class AbstractSqlEamDb implements EamDb { * @throws EamDbException */ @Override - public List getArtifactInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException { - if (aType == null) { - throw new EamDbException("Correlation type is null"); - } + public List getArtifactInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException { + + String normalizedValue = CorrelationAttributeNormalizer.normalize(aType, value); + Connection conn = connect(); List artifactInstances = new ArrayList<>(); @@ -743,7 +743,7 @@ abstract class AbstractSqlEamDb implements EamDb { try { preparedStatement = conn.prepareStatement(sql); - preparedStatement.setString(1, value); + preparedStatement.setString(1, normalizedValue); resultSet = preparedStatement.executeQuery(); while (resultSet.next()) { artifactInstance = getEamArtifactInstanceFromResultSet(resultSet, aType); @@ -809,8 +809,12 @@ abstract class AbstractSqlEamDb implements EamDb { preparedStatement.setString(1, filePath.toLowerCase()); resultSet = preparedStatement.executeQuery(); while (resultSet.next()) { - artifactInstance = getEamArtifactInstanceFromResultSet(resultSet, aType); - artifactInstances.add(artifactInstance); + try { + artifactInstance = getEamArtifactInstanceFromResultSet(resultSet, aType); + artifactInstances.add(artifactInstance); + } catch (CorrelationAttributeNormalizationException ex) { + logger.log(Level.INFO, "Unable to get artifact instance from resultset.", ex); + } } } catch (SQLException ex) { throw new EamDbException("Error getting artifact instances by artifactType and artifactValue.", ex); // NON-NLS @@ -834,13 +838,8 @@ abstract class AbstractSqlEamDb implements EamDb { * ArtifactValue. */ @Override - public Long getCountArtifactInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException { - if (aType == null) { - throw new EamDbException("Correlation type is null"); - } - if (value == null) { - throw new EamDbException("Correlation value is null"); - } + public Long getCountArtifactInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException { + String normalizedValue = CorrelationAttributeNormalizer.normalize(aType, value); Connection conn = connect(); @@ -856,7 +855,7 @@ abstract class AbstractSqlEamDb implements EamDb { try { preparedStatement = conn.prepareStatement(sql); - preparedStatement.setString(1, value.toLowerCase()); + preparedStatement.setString(1, normalizedValue); resultSet = preparedStatement.executeQuery(); resultSet.next(); instanceCount = resultSet.getLong(1); @@ -872,7 +871,7 @@ abstract class AbstractSqlEamDb implements EamDb { } @Override - public int getFrequencyPercentage(CorrelationAttributeInstance corAttr) throws EamDbException { + public int getFrequencyPercentage(CorrelationAttributeInstance corAttr) throws EamDbException, CorrelationAttributeNormalizationException { if (corAttr == null) { throw new EamDbException("CorrelationAttribute is null"); } @@ -893,10 +892,8 @@ abstract class AbstractSqlEamDb implements EamDb { * @return Number of unique tuples */ @Override - public Long getCountUniqueCaseDataSourceTuplesHavingTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException { - if (aType == null) { - throw new EamDbException("Correlation type is null"); - } + public Long getCountUniqueCaseDataSourceTuplesHavingTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException { + String normalizedValue = CorrelationAttributeNormalizer.normalize(aType, value); Connection conn = connect(); @@ -914,7 +911,7 @@ abstract class AbstractSqlEamDb implements EamDb { try { preparedStatement = conn.prepareStatement(sql); - preparedStatement.setString(1, value); + preparedStatement.setString(1, normalizedValue); resultSet = preparedStatement.executeQuery(); resultSet.next(); instanceCount = resultSet.getLong(1); @@ -1262,7 +1259,7 @@ abstract class AbstractSqlEamDb implements EamDb { preparedQuery.setString(1, eamArtifact.getComment()); preparedQuery.setString(2, eamArtifact.getCorrelationCase().getCaseUUID()); preparedQuery.setString(3, eamArtifact.getCorrelationDataSource().getDeviceID()); - preparedQuery.setString(4, eamArtifact.getCorrelationValue().toLowerCase()); + preparedQuery.setString(4, eamArtifact.getCorrelationValue()); preparedQuery.setString(5, eamArtifact.getFilePath().toLowerCase()); preparedQuery.executeUpdate(); } catch (SQLException ex) { @@ -1289,20 +1286,14 @@ abstract class AbstractSqlEamDb implements EamDb { */ @Override public CorrelationAttributeInstance getCorrelationAttributeInstance(CorrelationAttributeInstance.Type type, CorrelationCase correlationCase, - CorrelationDataSource correlationDataSource, String value, String filePath) throws EamDbException { - - if (type == null) { - throw new EamDbException("Correlation type is null"); - } + CorrelationDataSource correlationDataSource, String value, String filePath) throws EamDbException, CorrelationAttributeNormalizationException { + if (correlationCase == null) { throw new EamDbException("Correlation case is null"); } if (correlationDataSource == null) { throw new EamDbException("Correlation data source is null"); } - if (value == null) { - throw new EamDbException("Correlation value is null"); - } if (filePath == null) { throw new EamDbException("Correlation file path is null"); } @@ -1314,6 +1305,8 @@ abstract class AbstractSqlEamDb implements EamDb { CorrelationAttributeInstance correlationAttributeInstance = null; try { + String normalizedValue = CorrelationAttributeNormalizer.normalize(type, value); + String tableName = EamDbUtil.correlationTypeToInstanceTableName(type); String sql = "SELECT id, known_status, comment FROM " @@ -1326,7 +1319,7 @@ abstract class AbstractSqlEamDb implements EamDb { preparedStatement = conn.prepareStatement(sql); preparedStatement.setInt(1, correlationCase.getID()); preparedStatement.setInt(2, correlationDataSource.getID()); - preparedStatement.setString(3, value.toLowerCase()); + preparedStatement.setString(3, normalizedValue); preparedStatement.setString(4, filePath.toLowerCase()); resultSet = preparedStatement.executeQuery(); if (resultSet.next()) { @@ -1457,10 +1450,8 @@ abstract class AbstractSqlEamDb implements EamDb { * @return List with 0 or more matching eamArtifact instances. */ @Override - public List getArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException { - if (aType == null) { - throw new EamDbException("Correlation type is null"); - } + public List getArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException { + String normalizedValue = CorrelationAttributeNormalizer.normalize(aType, value); Connection conn = connect(); @@ -1489,7 +1480,7 @@ abstract class AbstractSqlEamDb implements EamDb { try { preparedStatement = conn.prepareStatement(sql); - preparedStatement.setString(1, value); + preparedStatement.setString(1, normalizedValue); preparedStatement.setByte(2, TskData.FileKnown.BAD.getFileKnownValue()); resultSet = preparedStatement.executeQuery(); while (resultSet.next()) { @@ -1552,8 +1543,12 @@ abstract class AbstractSqlEamDb implements EamDb { preparedStatement.setByte(1, TskData.FileKnown.BAD.getFileKnownValue()); resultSet = preparedStatement.executeQuery(); while (resultSet.next()) { - artifactInstance = getEamArtifactInstanceFromResultSet(resultSet, aType); - artifactInstances.add(artifactInstance); + try { + artifactInstance = getEamArtifactInstanceFromResultSet(resultSet, aType); + artifactInstances.add(artifactInstance); + } catch (CorrelationAttributeNormalizationException ex) { + logger.log(Level.INFO, "Unable to get artifact instance from resultset.", ex); + } } } catch (SQLException ex) { throw new EamDbException("Error getting notable artifact instances.", ex); // NON-NLS @@ -1575,10 +1570,9 @@ abstract class AbstractSqlEamDb implements EamDb { * @return Number of matching eamArtifacts */ @Override - public Long getCountArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException { - if (aType == null) { - throw new EamDbException("Correlation type is null"); - } + public Long getCountArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException { + + String normalizedValue = CorrelationAttributeNormalizer.normalize(aType, value); Connection conn = connect(); @@ -1594,7 +1588,7 @@ abstract class AbstractSqlEamDb implements EamDb { try { preparedStatement = conn.prepareStatement(sql); - preparedStatement.setString(1, value); + preparedStatement.setString(1, normalizedValue); preparedStatement.setByte(2, TskData.FileKnown.BAD.getFileKnownValue()); resultSet = preparedStatement.executeQuery(); resultSet.next(); @@ -1623,10 +1617,9 @@ abstract class AbstractSqlEamDb implements EamDb { * @throws EamDbException */ @Override - public List getListCasesHavingArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException { - if (aType == null) { - throw new EamDbException("Correlation type is null"); - } + public List getListCasesHavingArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException { + + String normalizedValue = CorrelationAttributeNormalizer.normalize(aType, value); Connection conn = connect(); @@ -1649,7 +1642,7 @@ abstract class AbstractSqlEamDb implements EamDb { try { preparedStatement = conn.prepareStatement(sql); - preparedStatement.setString(1, value); + preparedStatement.setString(1, normalizedValue); preparedStatement.setByte(2, TskData.FileKnown.BAD.getFileKnownValue()); resultSet = preparedStatement.executeQuery(); while (resultSet.next()) { @@ -1768,7 +1761,7 @@ abstract class AbstractSqlEamDb implements EamDb { * @throws EamDbException */ @Override - public boolean isFileHashInReferenceSet(String hash, int referenceSetID) throws EamDbException { + public boolean isFileHashInReferenceSet(String hash, int referenceSetID) throws EamDbException, CorrelationAttributeNormalizationException { return isValueInReferenceSet(hash, referenceSetID, CorrelationAttributeInstance.FILES_TYPE_ID); } @@ -1782,8 +1775,10 @@ abstract class AbstractSqlEamDb implements EamDb { * @return true if the value is found in the reference set */ @Override - public boolean isValueInReferenceSet(String value, int referenceSetID, int correlationTypeID) throws EamDbException { + public boolean isValueInReferenceSet(String value, int referenceSetID, int correlationTypeID) throws EamDbException, CorrelationAttributeNormalizationException { + String normalizeValued = CorrelationAttributeNormalizer.normalize(this.getCorrelationTypeById(correlationTypeID), value); + Connection conn = connect(); Long matchingInstances = 0L; @@ -1795,13 +1790,13 @@ abstract class AbstractSqlEamDb implements EamDb { try { preparedStatement = conn.prepareStatement(String.format(sql, fileTableName)); - preparedStatement.setString(1, value); + preparedStatement.setString(1, normalizeValued); preparedStatement.setInt(2, referenceSetID); resultSet = preparedStatement.executeQuery(); resultSet.next(); matchingInstances = resultSet.getLong(1); } catch (SQLException ex) { - throw new EamDbException("Error determining if value (" + value + ") is in reference set " + referenceSetID, ex); // NON-NLS + throw new EamDbException("Error determining if value (" + normalizeValued + ") is in reference set " + referenceSetID, ex); // NON-NLS } finally { EamDbUtil.closeStatement(preparedStatement); EamDbUtil.closeResultSet(resultSet); @@ -1820,11 +1815,11 @@ abstract class AbstractSqlEamDb implements EamDb { * @return Global known status of the artifact */ @Override - public boolean isArtifactKnownBadByReference(CorrelationAttributeInstance.Type aType, String value) throws EamDbException { - if (aType == null) { - throw new EamDbException("Correlation type is null"); - } - + public boolean isArtifactKnownBadByReference(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException { + + //this should be done here so that we can be certain that aType and value are valid before we proceed + String normalizeValued = CorrelationAttributeNormalizer.normalize(aType, value); + // TEMP: Only support file correlation type if (aType.getId() != CorrelationAttributeInstance.FILES_TYPE_ID) { return false; @@ -1837,9 +1832,9 @@ abstract class AbstractSqlEamDb implements EamDb { ResultSet resultSet = null; String sql = "SELECT count(*) FROM %s WHERE value=? AND known_status=?"; - try { + try { preparedStatement = conn.prepareStatement(String.format(sql, EamDbUtil.correlationTypeToReferenceTableName(aType))); - preparedStatement.setString(1, value); + preparedStatement.setString(1, normalizeValued); preparedStatement.setByte(2, TskData.FileKnown.BAD.getFileKnownValue()); resultSet = preparedStatement.executeQuery(); resultSet.next(); @@ -2420,10 +2415,8 @@ abstract class AbstractSqlEamDb implements EamDb { * @throws EamDbException */ @Override - public List getReferenceInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String aValue) throws EamDbException { - if (aType == null) { - throw new EamDbException("Correlation type is null"); - } + public List getReferenceInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String aValue) throws EamDbException, CorrelationAttributeNormalizationException { + String normalizeValued = CorrelationAttributeNormalizer.normalize(aType, aValue); Connection conn = connect(); @@ -2434,12 +2427,11 @@ abstract class AbstractSqlEamDb implements EamDb { try { preparedStatement1 = conn.prepareStatement(String.format(sql1, EamDbUtil.correlationTypeToReferenceTableName(aType))); - preparedStatement1.setString(1, aValue); + preparedStatement1.setString(1, normalizeValued); resultSet = preparedStatement1.executeQuery(); while (resultSet.next()) { globalFileInstances.add(getEamGlobalFileInstanceFromResultSet(resultSet)); } - return globalFileInstances; } catch (SQLException ex) { throw new EamDbException("Error getting reference instances by type and value.", ex); // NON-NLS @@ -2448,6 +2440,8 @@ abstract class AbstractSqlEamDb implements EamDb { EamDbUtil.closeResultSet(resultSet); EamDbUtil.closeConnection(conn); } + + return globalFileInstances; } /** @@ -2828,7 +2822,7 @@ abstract class AbstractSqlEamDb implements EamDb { * * @throws SQLException when an expected column name is not in the resultSet */ - private CorrelationAttributeInstance getEamArtifactInstanceFromResultSet(ResultSet resultSet, CorrelationAttributeInstance.Type aType) throws SQLException, EamDbException { + private CorrelationAttributeInstance getEamArtifactInstanceFromResultSet(ResultSet resultSet, CorrelationAttributeInstance.Type aType) throws SQLException, EamDbException, CorrelationAttributeNormalizationException { if (null == resultSet) { return null; } @@ -2876,7 +2870,7 @@ abstract class AbstractSqlEamDb implements EamDb { ); } - private EamGlobalFileInstance getEamGlobalFileInstanceFromResultSet(ResultSet resultSet) throws SQLException, EamDbException { + private EamGlobalFileInstance getEamGlobalFileInstanceFromResultSet(ResultSet resultSet) throws SQLException, EamDbException, CorrelationAttributeNormalizationException { if (null == resultSet) { return null; } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeInstance.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeInstance.java index 9d64f7dc72..261f8c4112 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeInstance.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeInstance.java @@ -55,7 +55,7 @@ public class CorrelationAttributeInstance implements Serializable { CorrelationCase eamCase, CorrelationDataSource eamDataSource, String filePath - ) throws EamDbException { + ) throws EamDbException, CorrelationAttributeNormalizationException { this(correlationType, correlationValue, -1, eamCase, eamDataSource, filePath, null, TskData.FileKnown.UNKNOWN); } @@ -67,7 +67,7 @@ public class CorrelationAttributeInstance implements Serializable { String filePath, String comment, TskData.FileKnown knownStatus - ) throws EamDbException { + ) throws EamDbException, CorrelationAttributeNormalizationException { this(correlationType, correlationValue, -1, eamCase, eamDataSource, filePath, comment, knownStatus); } @@ -76,7 +76,7 @@ public class CorrelationAttributeInstance implements Serializable { String correlationValue, CorrelationCase correlationCase, CorrelationDataSource fromTSKDataSource, - String string) throws EamDbException { + String string) throws EamDbException, CorrelationAttributeNormalizationException { this(correlationType, correlationValue, -1, correlationCase, fromTSKDataSource, string, "", TskData.FileKnown.UNKNOWN); } @@ -86,7 +86,7 @@ public class CorrelationAttributeInstance implements Serializable { * @param aType CorrelationAttributeInstance.Type * @param value correlation value */ - public CorrelationAttributeInstance(Type aType, String value) throws EamDbException { + public CorrelationAttributeInstance(Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException { this(aType, value, -1, null, null, "", "", TskData.FileKnown.UNKNOWN); } @@ -99,17 +99,13 @@ public class CorrelationAttributeInstance implements Serializable { String filePath, String comment, TskData.FileKnown knownStatus - ) throws EamDbException { + ) throws EamDbException, CorrelationAttributeNormalizationException { if (filePath == null) { throw new EamDbException("file path is null"); } - if (value == null) { - throw new EamDbException("correlation value is null"); - } - this.correlationType = type; - this.correlationValue = value; + this.correlationValue = CorrelationAttributeNormalizer.normalize(type, value); this.ID = instanceId; this.correlationCase = eamCase; this.correlationDataSource = eamDataSource; @@ -121,6 +117,8 @@ public class CorrelationAttributeInstance implements Serializable { public Boolean equals(CorrelationAttributeInstance otherInstance) { return ((this.getID() == otherInstance.getID()) + && (this.getCorrelationValue().equals(otherInstance.getCorrelationValue())) + && (this.getCorrelationType().equals(otherInstance.getCorrelationType())) && (this.getCorrelationCase().equals(otherInstance.getCorrelationCase())) && (this.getCorrelationDataSource().equals(otherInstance.getCorrelationDataSource())) && (this.getFilePath().equals(otherInstance.getFilePath())) @@ -134,6 +132,8 @@ public class CorrelationAttributeInstance implements Serializable { + this.getCorrelationCase().getCaseUUID() + this.getCorrelationDataSource().getDeviceID() + this.getFilePath() + + this.getCorrelationType().toString() + + this.getCorrelationValue() + this.getKnownStatus() + this.getComment(); } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizationException.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizationException.java new file mode 100644 index 0000000000..7bdd56e4a3 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizationException.java @@ -0,0 +1,53 @@ +/* + * + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.centralrepository.datamodel; + +/** + * Thrown when a given value is not in the expected format. + */ +public class CorrelationAttributeNormalizationException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Construct an exception with the given message. + * @param message error message + */ + public CorrelationAttributeNormalizationException(String message){ + super(message); + } + + /** + * Construct an exception with the given message and inner exception. + * @param message error message + * @param cause inner exception + */ + public CorrelationAttributeNormalizationException(String message, Throwable cause){ + super(message, cause); + } + + /** + * Construct an exception with the given inner exception. + * @param cause inner exception + */ + public CorrelationAttributeNormalizationException(Throwable cause){ + super(cause); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizer.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizer.java new file mode 100644 index 0000000000..772e1c517e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizer.java @@ -0,0 +1,160 @@ +/* + * + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.centralrepository.datamodel; + +import java.util.List; +import java.util.Optional; +import org.apache.commons.validator.routines.DomainValidator; +import org.apache.commons.validator.routines.EmailValidator; + +/** + * Provides functions for normalizing data by attribute type before insertion or querying. + */ +final public class CorrelationAttributeNormalizer { + + /** + * This is a utility class - no need for constructing or subclassing, etc... + */ + private CorrelationAttributeNormalizer() { } + + /** + * Normalize the data. Converts text to lower case, and ensures that the + * data is a valid string of the format expected given the attributeType. + * + * @param attributeType correlation type of data + * @param data data to normalize + * + * @return normalized data + */ + public static String normalize(CorrelationAttributeInstance.Type attributeType, String data) throws CorrelationAttributeNormalizationException { + + if(attributeType == null){ + throw new CorrelationAttributeNormalizationException("Attribute type was null."); + } + if(data == null){ + throw new CorrelationAttributeNormalizationException("Data was null."); + } + + switch(attributeType.getId()){ + case CorrelationAttributeInstance.FILES_TYPE_ID: + return normalizeMd5(data); + case CorrelationAttributeInstance.DOMAIN_TYPE_ID: + return normalizeDomain(data); + case CorrelationAttributeInstance.EMAIL_TYPE_ID: + return normalizeEmail(data); + case CorrelationAttributeInstance.PHONE_TYPE_ID: + return normalizePhone(data); + case CorrelationAttributeInstance.USBID_TYPE_ID: + return normalizeUsbId(data); + default: + final String errorMessage = String.format( + "Validator function not found for attribute type: %s", + attributeType.getDisplayName()); + throw new CorrelationAttributeNormalizationException(errorMessage); + } + } + + /** + * Validate the data. Converts text to lower case, and ensures that the + * data is a valid string of the format expected given the attributeType. + * + * @param attributeTypeId correlation type of data + * @param data data to normalize + * + * @return normalized data + */ + public static String normalize(int attributeTypeId, String data) throws CorrelationAttributeNormalizationException { + try { + List defaultTypes = CorrelationAttributeInstance.getDefaultCorrelationTypes(); + Optional typeOption = defaultTypes.stream().filter(attributeType -> attributeType.getId() == attributeTypeId).findAny(); + + if(typeOption.isPresent()){ + CorrelationAttributeInstance.Type type = typeOption.get(); + return CorrelationAttributeNormalizer.normalize(type, data); + } else { + throw new CorrelationAttributeNormalizationException(String.format("Given attributeTypeId did not correspond to any known Attribute: %s", attributeTypeId)); + } + } catch (EamDbException ex) { + throw new CorrelationAttributeNormalizationException(ex); + } + } + + /** + * Verify MD5 is the correct length and values. Make lower case. + */ + private static String normalizeMd5(String data) throws CorrelationAttributeNormalizationException { + final String validMd5Regex = "^[a-f0-9]{32}$"; + final String dataLowered = data.toLowerCase(); + if(dataLowered.matches(validMd5Regex)){ + return dataLowered; + } else { + throw new CorrelationAttributeNormalizationException(String.format("Data purporting to be an MD5 was found not to comform to expected format: %s", data)); + } + } + + /** + * Verify there are no slashes or invalid domain name characters (such as '?' or \: ). Normalize to lower case. + */ + private static String normalizeDomain(String data) throws CorrelationAttributeNormalizationException { + DomainValidator validator = DomainValidator.getInstance(true); + if(validator.isValid(data)){ + return data.toLowerCase(); + } else { + final String validIpAddressRegex = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"; + if(data.matches(validIpAddressRegex)){ + return data; + } else { + throw new CorrelationAttributeNormalizationException(String.format("Data was expected to be a valid domain: %s", data)); + } + } + } + + /** + * Verify that there is an '@' and no invalid characters. Should normalize to lower case. + */ + private static String normalizeEmail(String data) throws CorrelationAttributeNormalizationException { + EmailValidator validator = EmailValidator.getInstance(true, true); + if(validator.isValid(data)){ + return data.toLowerCase(); + } else { + throw new CorrelationAttributeNormalizationException(String.format("Data was expected to be a valid email address: %s", data)); + } + } + + /** + * Verify it is only numbers and '+'. Strip spaces, dashes, and parentheses. + */ + private static String normalizePhone(String data) throws CorrelationAttributeNormalizationException { + if(data.matches("\\+?[0-9()\\-\\s]+")){ + String phoneNumber = data.replaceAll("[^0-9\\+]", ""); + return phoneNumber; + } else { + throw new CorrelationAttributeNormalizationException(String.format("Data was expected to be a valid phone number: %s", data)); + } + } + + /** + * Vacuous - will be replaced with something reasonable later. + */ + private static String normalizeUsbId(String data) throws CorrelationAttributeNormalizationException { + //TODO replace with correct usb id validation at a later date + return data; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamArtifactUtil.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamArtifactUtil.java index 48aea257ba..095c1a7139 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamArtifactUtil.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamArtifactUtil.java @@ -213,7 +213,7 @@ public class EamArtifactUtil { TskData.FileKnown.UNKNOWN ); - } catch (TskCoreException | EamDbException ex) { + } catch (TskCoreException | EamDbException | CorrelationAttributeNormalizationException ex) { logger.log(Level.SEVERE, "Error creating artifact instance.", ex); // NON-NLS return null; } catch (NoCurrentCaseException ex) { @@ -251,7 +251,8 @@ public class EamArtifactUtil { type = EamDb.getInstance().getCorrelationTypeById(CorrelationAttributeInstance.FILES_TYPE_ID); correlationCase = EamDb.getInstance().getCase(Case.getCurrentCaseThrows()); if (null == correlationCase) { - correlationCase = EamDb.getInstance().newCase(Case.getCurrentCaseThrows()); + //if the correlationCase is not in the Central repo then attributes generated in relation to it will not be + return null; } correlationDataSource = CorrelationDataSource.fromTSKDataSource(correlationCase, file.getDataSource()); value = file.getMd5Hash(); @@ -267,7 +268,7 @@ public class EamArtifactUtil { CorrelationAttributeInstance correlationAttributeInstance; try { correlationAttributeInstance = EamDb.getInstance().getCorrelationAttributeInstance(type, correlationCase, correlationDataSource, value, filePath); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { logger.log(Level.WARNING, String.format( "Correlation attribute could not be retrieved for '%s' (id=%d): %s", content.getName(), content.getId(), ex.getMessage())); @@ -322,7 +323,7 @@ public class EamArtifactUtil { CorrelationDataSource.fromTSKDataSource(correlationCase, af.getDataSource()), af.getParentPath() + af.getName()); - } catch (TskCoreException | EamDbException ex) { + } catch (TskCoreException | EamDbException | CorrelationAttributeNormalizationException ex) { logger.log(Level.SEVERE, "Error making correlation attribute.", ex); return null; } catch (NoCurrentCaseException ex) { diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamDb.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamDb.java index a58daa4b9b..d7b725109b 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamDb.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamDb.java @@ -244,7 +244,7 @@ public interface EamDb { * * @return List of artifact instances for a given type/value */ - List getArtifactInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException; + List getArtifactInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException; /** * Retrieves eamArtifact instances from the database that are associated @@ -269,7 +269,7 @@ public interface EamDb { * @return Number of artifact instances having ArtifactType and * ArtifactValue. */ - Long getCountArtifactInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException; + Long getCountArtifactInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException; /** * Calculate the percentage of data sources that have this attribute value. @@ -278,7 +278,7 @@ public interface EamDb { * * @return Int between 0 and 100 */ - int getFrequencyPercentage(CorrelationAttributeInstance corAttr) throws EamDbException; + int getFrequencyPercentage(CorrelationAttributeInstance corAttr) throws EamDbException, CorrelationAttributeNormalizationException; /** * Retrieves number of unique caseDisplayName / dataSource tuples in the @@ -290,7 +290,7 @@ public interface EamDb { * * @return Number of unique tuples */ - Long getCountUniqueCaseDataSourceTuplesHavingTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException; + Long getCountUniqueCaseDataSourceTuplesHavingTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException; /** * Retrieves number of data sources in the database. @@ -358,7 +358,7 @@ public interface EamDb { * @throws EamDbException */ CorrelationAttributeInstance getCorrelationAttributeInstance(CorrelationAttributeInstance.Type type, CorrelationCase correlationCase, - CorrelationDataSource correlationDataSource, String value, String filePath) throws EamDbException; + CorrelationDataSource correlationDataSource, String value, String filePath) throws EamDbException, CorrelationAttributeNormalizationException; /** * Sets an eamArtifact instance to the given known status. If eamArtifact @@ -378,7 +378,7 @@ public interface EamDb { * * @return List with 0 or more matching eamArtifact instances. */ - List getArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException; + List getArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException; /** * Gets list of matching eamArtifact instances that have knownStatus = @@ -397,7 +397,7 @@ public interface EamDb { * * @return Number of matching eamArtifacts */ - Long getCountArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException; + Long getCountArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException; /** * Gets list of distinct case display names, where each case has 1+ Artifact @@ -411,7 +411,7 @@ public interface EamDb { * * @throws EamDbException */ - List getListCasesHavingArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException; + List getListCasesHavingArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException; /** * Remove a reference set and all values contained in it. @@ -462,7 +462,7 @@ public interface EamDb { * * @throws EamDbException */ - public boolean isFileHashInReferenceSet(String hash, int referenceSetID) throws EamDbException; + public boolean isFileHashInReferenceSet(String hash, int referenceSetID) throws EamDbException, CorrelationAttributeNormalizationException; /** * Check if the given value is in a specific reference set @@ -473,7 +473,7 @@ public interface EamDb { * * @return true if the hash is found in the reference set */ - public boolean isValueInReferenceSet(String value, int referenceSetID, int correlationTypeID) throws EamDbException; + public boolean isValueInReferenceSet(String value, int referenceSetID, int correlationTypeID) throws EamDbException, CorrelationAttributeNormalizationException; /** * Is the artifact known as bad according to the reference entries? @@ -483,7 +483,7 @@ public interface EamDb { * * @return Global known status of the artifact */ - boolean isArtifactKnownBadByReference(CorrelationAttributeInstance.Type aType, String value) throws EamDbException; + boolean isArtifactKnownBadByReference(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException; /** * Add a new organization @@ -611,7 +611,7 @@ public interface EamDb { * * @throws EamDbException */ - List getReferenceInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String aValue) throws EamDbException; + List getReferenceInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String aValue) throws EamDbException, CorrelationAttributeNormalizationException; /** * Add a new EamArtifact.Type to the db. diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamGlobalFileInstance.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamGlobalFileInstance.java index 3c538e67c8..9f51752251 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamGlobalFileInstance.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamGlobalFileInstance.java @@ -36,7 +36,7 @@ public class EamGlobalFileInstance { int globalSetID, String MD5Hash, TskData.FileKnown knownStatus, - String comment) throws EamDbException { + String comment) throws EamDbException, CorrelationAttributeNormalizationException { this(-1, globalSetID, MD5Hash, knownStatus, comment); } @@ -45,17 +45,14 @@ public class EamGlobalFileInstance { int globalSetID, String MD5Hash, TskData.FileKnown knownStatus, - String comment) throws EamDbException { - if(MD5Hash == null){ - throw new EamDbException("null MD5 hash"); - } + String comment) throws EamDbException, CorrelationAttributeNormalizationException { + if(knownStatus == null){ throw new EamDbException("null known status"); } this.instanceID = instanceID; this.globalSetID = globalSetID; - // Normalize hashes by lower casing - this.MD5Hash = MD5Hash.toLowerCase(); + this.MD5Hash = CorrelationAttributeNormalizer.normalize(CorrelationAttributeInstance.FILES_TYPE_ID, MD5Hash); this.knownStatus = knownStatus; this.comment = comment; } @@ -117,12 +114,8 @@ public class EamGlobalFileInstance { /** * @param MD5Hash the MD5Hash to set */ - public void setMD5Hash(String MD5Hash) throws EamDbException { - if(MD5Hash == null){ - throw new EamDbException("null MD5 hash"); - } - // Normalize hashes by lower casing - this.MD5Hash = MD5Hash.toLowerCase(); + public void setMD5Hash(String MD5Hash) throws CorrelationAttributeNormalizationException { + this.MD5Hash = CorrelationAttributeNormalizer.normalize(CorrelationAttributeInstance.FILES_TYPE_ID, MD5Hash); } /** diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/InstanceTableCallback.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/InstanceTableCallback.java index d7da43bbf4..6816bebb32 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/InstanceTableCallback.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/InstanceTableCallback.java @@ -106,4 +106,5 @@ public interface InstanceTableCallback { return resultSet.getString("comment"); } + } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteEamDb.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteEamDb.java index d300964b5f..4805e4e0b0 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteEamDb.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteEamDb.java @@ -447,7 +447,7 @@ final class SqliteEamDb extends AbstractSqlEamDb { * @return List of artifact instances for a given type/value */ @Override - public List getArtifactInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException { + public List getArtifactInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException { try { acquireSharedLock(); return super.getArtifactInstancesByTypeValue(aType, value); @@ -489,7 +489,7 @@ final class SqliteEamDb extends AbstractSqlEamDb { * @throws EamDbException */ @Override - public Long getCountArtifactInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException { + public Long getCountArtifactInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException { try { acquireSharedLock(); return super.getCountArtifactInstancesByTypeValue(aType, value); @@ -499,7 +499,7 @@ final class SqliteEamDb extends AbstractSqlEamDb { } @Override - public int getFrequencyPercentage(CorrelationAttributeInstance corAttr) throws EamDbException { + public int getFrequencyPercentage(CorrelationAttributeInstance corAttr) throws EamDbException, CorrelationAttributeNormalizationException { try { acquireSharedLock(); return super.getFrequencyPercentage(corAttr); @@ -520,7 +520,7 @@ final class SqliteEamDb extends AbstractSqlEamDb { * @throws EamDbException */ @Override - public Long getCountUniqueCaseDataSourceTuplesHavingTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException { + public Long getCountUniqueCaseDataSourceTuplesHavingTypeValue(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException { try { acquireSharedLock(); return super.getCountUniqueCaseDataSourceTuplesHavingTypeValue(aType, value); @@ -617,7 +617,7 @@ final class SqliteEamDb extends AbstractSqlEamDb { * @return List with 0 or more matching eamArtifact instances. */ @Override - public List getArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException { + public List getArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException { try { acquireSharedLock(); return super.getArtifactInstancesKnownBad(aType, value); @@ -654,7 +654,7 @@ final class SqliteEamDb extends AbstractSqlEamDb { * @return Number of matching eamArtifacts */ @Override - public Long getCountArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException { + public Long getCountArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException { try { acquireSharedLock(); return super.getCountArtifactInstancesKnownBad(aType, value); @@ -676,7 +676,7 @@ final class SqliteEamDb extends AbstractSqlEamDb { * @throws EamDbException */ @Override - public List getListCasesHavingArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException { + public List getListCasesHavingArtifactInstancesKnownBad(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException { try { acquireSharedLock(); return super.getListCasesHavingArtifactInstancesKnownBad(aType, value); @@ -710,7 +710,7 @@ final class SqliteEamDb extends AbstractSqlEamDb { * @return true if the hash is found in the reference set */ @Override - public boolean isValueInReferenceSet(String value, int referenceSetID, int correlationTypeID) throws EamDbException { + public boolean isValueInReferenceSet(String value, int referenceSetID, int correlationTypeID) throws EamDbException, CorrelationAttributeNormalizationException { try { acquireSharedLock(); return super.isValueInReferenceSet(value, referenceSetID, correlationTypeID); @@ -782,7 +782,7 @@ final class SqliteEamDb extends AbstractSqlEamDb { * @return Global known status of the artifact */ @Override - public boolean isArtifactKnownBadByReference(CorrelationAttributeInstance.Type aType, String value) throws EamDbException { + public boolean isArtifactKnownBadByReference(CorrelationAttributeInstance.Type aType, String value) throws EamDbException, CorrelationAttributeNormalizationException { try { acquireSharedLock(); return super.isArtifactKnownBadByReference(aType, value); @@ -967,7 +967,7 @@ final class SqliteEamDb extends AbstractSqlEamDb { * @throws EamDbException */ @Override - public List getReferenceInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String aValue) throws EamDbException { + public List getReferenceInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String aValue) throws EamDbException, CorrelationAttributeNormalizationException { try { acquireSharedLock(); return super.getReferenceInstancesByTypeValue(aType, aValue); diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/IngestEventsListener.java b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/IngestEventsListener.java index e432c4bf6b..ccca659574 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/IngestEventsListener.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/IngestEventsListener.java @@ -34,6 +34,7 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.services.Blackboard; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.IngestServices; @@ -123,19 +124,19 @@ public class IngestEventsListener { public synchronized static int getCeModuleInstanceCount() { return correlationModuleInstanceCount; } - + /** * Are notable items being flagged? - * + * * @return True if flagging notable items; otherwise false. */ public synchronized static boolean isFlagNotableItems() { return flagNotableItems; } - + /** * Configure the listener to flag notable items or not. - * + * * @param value True to flag notable items; otherwise false. */ public synchronized static void setFlagNotableItems(boolean value) { @@ -259,13 +260,18 @@ public class IngestEventsListener { if (recentlyAddedCeArtifacts.add(eamArtifact.toString())) { // Was it previously marked as bad? // query db for artifact instances having this TYPE/VALUE and knownStatus = "Bad". - // if gettKnownStatus() is "Unknown" and this artifact instance was marked bad in a previous case, + // if getKnownStatus() is "Unknown" and this artifact instance was marked bad in a previous case, // create TSK_INTERESTING_ARTIFACT_HIT artifact on BB. if (flagNotableItemsEnabled) { - List caseDisplayNames = dbManager.getListCasesHavingArtifactInstancesKnownBad(eamArtifact.getCorrelationType(), eamArtifact.getCorrelationValue()); - if (!caseDisplayNames.isEmpty()) { - postCorrelatedBadArtifactToBlackboard(bbArtifact, - caseDisplayNames); + List caseDisplayNames; + try { + caseDisplayNames = dbManager.getListCasesHavingArtifactInstancesKnownBad(eamArtifact.getCorrelationType(), eamArtifact.getCorrelationValue()); + if (!caseDisplayNames.isEmpty()) { + postCorrelatedBadArtifactToBlackboard(bbArtifact, + caseDisplayNames); + } + } catch (CorrelationAttributeNormalizationException ex) { + LOGGER.log(Level.INFO, String.format("Unable to flag notable item: %s.", eamArtifact.toString()), ex); } } eamArtifacts.add(eamArtifact); diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestModule.java b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestModule.java index 441380bcc6..21baf59454 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestModule.java @@ -28,6 +28,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.services.Blackboard; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException; import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.ingest.FileIngestModule; @@ -143,6 +144,9 @@ final class IngestModule implements FileIngestModule { } catch (EamDbException ex) { logger.log(Level.SEVERE, "Error searching database for artifact.", ex); // NON-NLS return ProcessResult.ERROR; + } catch (CorrelationAttributeNormalizationException ex){ + logger.log(Level.INFO, "Error searching database for artifact.", ex); // NON-NLS + return ProcessResult.ERROR; } } @@ -161,6 +165,9 @@ final class IngestModule implements FileIngestModule { } catch (EamDbException ex) { logger.log(Level.SEVERE, "Error adding artifact to bulk artifacts.", ex); // NON-NLS return ProcessResult.ERROR; + } catch (CorrelationAttributeNormalizationException ex) { + logger.log(Level.INFO, "Error adding artifact to bulk artifacts.", ex); // NON-NLS + return ProcessResult.ERROR; } return ProcessResult.OK; diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/AbstractCommonAttributeInstance.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/AbstractCommonAttributeInstance.java index c8ecb3225b..7f4d6605ab 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/AbstractCommonAttributeInstance.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/AbstractCommonAttributeInstance.java @@ -71,6 +71,12 @@ public abstract class AbstractCommonAttributeInstance { this.caseName = ""; this.dataSource = ""; } + + /** + * Get the type of common attribute. + * @return + */ + public abstract CorrelationAttributeInstance.Type getCorrelationAttributeInstanceType(); /** * Get an AbstractFile for this instance if it can be retrieved from the diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/AbstractCommonAttributeSearcher.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/AbstractCommonAttributeSearcher.java index 62cbbeb24b..f79737c116 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/AbstractCommonAttributeSearcher.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/AbstractCommonAttributeSearcher.java @@ -1,16 +1,16 @@ /* - * + * * Autopsy Forensic Browser - * + * * Copyright 2018 Basis Technology Corp. * Contact: carrier sleuthkit org - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -34,59 +34,57 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; import org.sleuthkit.datamodel.TskCoreException; /** - * Prototype for an object which finds files with common attributes. - * Subclass this and implement findFiles in order + * Prototype for an object which finds files with common attributes. Subclass + * this and implement findMatches in order */ public abstract class AbstractCommonAttributeSearcher { - + private final Map dataSourceIdToNameMap; private boolean filterByMedia; private boolean filterByDoc; final int frequencyPercentageThreshold; - - AbstractCommonAttributeSearcher(Map dataSourceIdMap, boolean filterByMedia, boolean filterByDoc, int percentageThreshold){ + + AbstractCommonAttributeSearcher(Map dataSourceIdMap, boolean filterByMedia, boolean filterByDoc, int percentageThreshold) { this.filterByDoc = filterByDoc; this.filterByMedia = filterByMedia; this.dataSourceIdToNameMap = dataSourceIdMap; this.frequencyPercentageThreshold = percentageThreshold; } - - Map getDataSourceIdToNameMap(){ + + Map getDataSourceIdToNameMap() { return Collections.unmodifiableMap(this.dataSourceIdToNameMap); } - + /** - * Implement this to search for files with common attributes. Creates an - * object (CommonAttributeSearchResults) which contains all of the information - * required to display a tree view in the UI. The view will contain 3 layers: - * a top level node, indicating the number matches each of it's children possess, - * a mid level node indicating the matched attribute, + * Implement this to search for files with common attributes. Creates an + * object (CommonAttributeSearchResults) which contains all of the + * information required to display a tree view in the UI. The view will + * contain 3 layers: a top level node, indicating the number matches each of + * it's children possess, a mid level node indicating the matched attribute, + * * @return + * * @throws TskCoreException * @throws NoCurrentCaseException * @throws SQLException - * @throws EamDbException + * @throws EamDbException */ - public abstract CommonAttributeSearchResults findFiles() throws TskCoreException, NoCurrentCaseException, SQLException, EamDbException; - + public abstract CommonAttributeSearchResults findMatches() throws TskCoreException, NoCurrentCaseException, SQLException, EamDbException; + /** - * Implement this to create a descriptive string for the tab which will display - * this data. + * Implement this to create a descriptive string for the tab which will + * display this data. + * * @return an informative string */ - @NbBundle.Messages({ - "AbstractCommonFilesMetadataBuilder.buildTabTitle.titleIntraAll=Common Files (All Data Sources, %s)", - "AbstractCommonFilesMetadataBuilder.buildTabTitle.titleIntraSingle=Common Files (Data Source: %s, %s)", - "AbstractCommonFilesMetadataBuilder.buildTabTitle.titleInterAll=Common Files (All Central Repository Cases, %s)", - "AbstractCommonFilesMetadataBuilder.buildTabTitle.titleInterSingle=Common Files (Central Repository Case: %s, %s)", - }) - abstract String buildTabTitle(); - + abstract String getTabTitle(); + @NbBundle.Messages({ "AbstractCommonFilesMetadataBuilder.buildCategorySelectionString.doc=Documents", "AbstractCommonFilesMetadataBuilder.buildCategorySelectionString.media=Media", "AbstractCommonFilesMetadataBuilder.buildCategorySelectionString.all=All File Categories" }) + String buildCategorySelectionString() { if (!this.isFilterByDoc() && !this.isFilterByMedia()) { return Bundle.AbstractCommonFilesMetadataBuilder_buildCategorySelectionString_all(); @@ -101,15 +99,33 @@ public abstract class AbstractCommonAttributeSearcher { return String.join(", ", filters); } } - + + /** + * Get the portion of the title that will display the frequency percentage + * threshold. Items that existed in over this percent of data sources were + * ommited from the results. + * + * @return A string providing the frequency percentage threshold, or an empty string if no threshold was set + */ + @NbBundle.Messages({ + "# {0} - threshold percent", + "AbstractCommonFilesMetadataBuilder.getPercentFilter.thresholdPercent=, Threshold {0}%"}) + String getPercentThresholdString() { + if (frequencyPercentageThreshold == 0) { + return ""; + } else { + return Bundle.AbstractCommonFilesMetadataBuilder_getPercentFilter_thresholdPercent(frequencyPercentageThreshold); + } + } + static Map collateMatchesByNumberOfInstances(Map commonFiles) { //collate matches by number of matching instances - doing this in sql doesnt seem efficient Map instanceCollatedCommonFiles = new TreeMap<>(); - - for(CommonAttributeValue md5Metadata : commonFiles.values()){ + + for (CommonAttributeValue md5Metadata : commonFiles.values()) { Integer size = md5Metadata.getInstanceCount(); - - if(instanceCollatedCommonFiles.containsKey(size)){ + + if (instanceCollatedCommonFiles.containsKey(size)) { instanceCollatedCommonFiles.get(size).addMetadataToList(md5Metadata); } else { CommonAttributeValueList value = new CommonAttributeValueList(); @@ -119,13 +135,13 @@ public abstract class AbstractCommonAttributeSearcher { } return instanceCollatedCommonFiles; } - + /* * The set of the MIME types that will be checked for extension mismatches - * when checkType is ONLY_MEDIA. - * ".jpg", ".jpeg", ".png", ".psd", ".nef", ".tiff", ".bmp", ".tec" - * ".aaf", ".3gp", ".asf", ".avi", ".m1v", ".m2v", //NON-NLS - * ".m4v", ".mp4", ".mov", ".mpeg", ".mpg", ".mpe", ".mp4", ".rm", ".wmv", ".mpv", ".flv", ".swf" + * when checkType is ONLY_MEDIA. ".jpg", ".jpeg", ".png", ".psd", ".nef", + * ".tiff", ".bmp", ".tec" ".aaf", ".3gp", ".asf", ".avi", ".m1v", ".m2v", + * //NON-NLS ".m4v", ".mp4", ".mov", ".mpeg", ".mpg", ".mpe", ".mp4", ".rm", + * ".wmv", ".mpv", ".flv", ".swf" */ static final Set MEDIA_PICS_VIDEO_MIME_TYPES = Stream.of( "image/bmp", //NON-NLS @@ -155,11 +171,9 @@ public abstract class AbstractCommonAttributeSearcher { /* * The set of the MIME types that will be checked for extension mismatches - * when checkType is ONLY_TEXT_FILES. - * ".doc", ".docx", ".odt", ".xls", ".xlsx", ".ppt", ".pptx" - * ".txt", ".rtf", ".log", ".text", ".xml" - * ".html", ".htm", ".css", ".js", ".php", ".aspx" - * ".pdf" + * when checkType is ONLY_TEXT_FILES. ".doc", ".docx", ".odt", ".xls", + * ".xlsx", ".ppt", ".pptx" ".txt", ".rtf", ".log", ".text", ".xml" ".html", + * ".htm", ".css", ".js", ".php", ".aspx" ".pdf" */ static final Set TEXT_FILES_MIME_TYPES = Stream.of( "text/plain", //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/AllInterCaseCommonAttributeSearcher.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/AllInterCaseCommonAttributeSearcher.java index ac84d0188f..f28d636be0 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/AllInterCaseCommonAttributeSearcher.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/AllInterCaseCommonAttributeSearcher.java @@ -1,16 +1,16 @@ /* - * + * * Autopsy Forensic Browser - * + * * Copyright 2018 Basis Technology Corp. * Contact: carrier sleuthkit org - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,12 +20,13 @@ package org.sleuthkit.autopsy.commonfilesearch; import java.sql.SQLException; -import java.util.List; import java.util.Map; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance.Type; /** * Algorithm which finds files anywhere in the Central Repo which also occur in @@ -34,28 +35,31 @@ import org.sleuthkit.datamodel.TskCoreException; public class AllInterCaseCommonAttributeSearcher extends InterCaseCommonAttributeSearcher { /** - * + * * @param filterByMediaMimeType match only on files whose mime types can be - * broadly categorized as media types - * @param filterByDocMimeType match only on files whose mime types can be - * broadly categorized as document types - * @throws EamDbException + * broadly categorized as media types + * @param filterByDocMimeType match only on files whose mime types can be + * broadly categorized as document types + * + * @throws EamDbException */ - public AllInterCaseCommonAttributeSearcher(Map dataSourceIdMap, boolean filterByMediaMimeType, boolean filterByDocMimeType, int percentageThreshold) throws EamDbException { - super(dataSourceIdMap, filterByMediaMimeType, filterByDocMimeType, percentageThreshold); + public AllInterCaseCommonAttributeSearcher(Map dataSourceIdMap, boolean filterByMediaMimeType, boolean filterByDocMimeType, Type corAttrType, int percentageThreshold) throws EamDbException { + super(dataSourceIdMap, filterByMediaMimeType, filterByDocMimeType, corAttrType, percentageThreshold); } @Override - public CommonAttributeSearchResults findFiles() throws TskCoreException, NoCurrentCaseException, SQLException, EamDbException { - InterCaseSearchResultsProcessor eamDbAttrInst = new InterCaseSearchResultsProcessor(this.getDataSourceIdToNameMap()); + public CommonAttributeSearchResults findMatches() throws TskCoreException, NoCurrentCaseException, SQLException, EamDbException { + InterCaseSearchResultsProcessor eamDbAttrInst = new InterCaseSearchResultsProcessor(this.getDataSourceIdToNameMap(), corAttrType); Map interCaseCommonFiles = eamDbAttrInst.findInterCaseCommonAttributeValues(Case.getCurrentCase()); - return new CommonAttributeSearchResults(interCaseCommonFiles, this.frequencyPercentageThreshold); + return new CommonAttributeSearchResults(interCaseCommonFiles, this.frequencyPercentageThreshold, this.corAttrType); } + @NbBundle.Messages({ + "# {0} - attr type", + "# {1} - threshold string", + "AllInterCaseCommonAttributeSearcher.buildTabTitle.titleInterAll=Common Properties (All Central Repository Cases, {0}{1})"}) @Override - String buildTabTitle() { - final String buildCategorySelectionString = this.buildCategorySelectionString(); - final String titleTemplate = Bundle.AbstractCommonFilesMetadataBuilder_buildTabTitle_titleInterAll(); - return String.format(titleTemplate, new Object[]{buildCategorySelectionString}); + String getTabTitle() { + return Bundle.AllInterCaseCommonAttributeSearcher_buildTabTitle_titleInterAll(this.corAttrType.getDisplayName(), this.getPercentThresholdString()); } } diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/AllIntraCaseCommonAttributeSearcher.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/AllIntraCaseCommonAttributeSearcher.java index 35a5c70452..a02e16cfd8 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/AllIntraCaseCommonAttributeSearcher.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/AllIntraCaseCommonAttributeSearcher.java @@ -1,16 +1,16 @@ /* - * + * * Autopsy Forensic Browser - * + * * Copyright 2018 Basis Technology Corp. * Contact: carrier sleuthkit org - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,6 +20,7 @@ package org.sleuthkit.autopsy.commonfilesearch; import java.util.Map; +import org.openide.util.NbBundle; import org.sleuthkit.datamodel.TskData.FileKnown; /** @@ -27,15 +28,17 @@ import org.sleuthkit.datamodel.TskData.FileKnown; */ final public class AllIntraCaseCommonAttributeSearcher extends IntraCaseCommonAttributeSearcher { - private static final String WHERE_CLAUSE = "%s md5 in (select md5 from tsk_files where (known != "+ FileKnown.KNOWN.getFileKnownValue() + " OR known IS NULL)%s GROUP BY md5 HAVING COUNT(DISTINCT data_source_obj_id) > 1) order by md5"; //NON-NLS + private static final String WHERE_CLAUSE = "%s md5 in (select md5 from tsk_files where (known != " + FileKnown.KNOWN.getFileKnownValue() + " OR known IS NULL)%s GROUP BY md5 HAVING COUNT(DISTINCT data_source_obj_id) > 1) order by md5"; //NON-NLS /** * Implements the algorithm for getting common files across all data * sources. * - * @param dataSourceIdMap a map of obj_id to datasource name - * @param filterByMediaMimeType match only on files whose mime types can be broadly categorized as media types - * @param filterByDocMimeType match only on files whose mime types can be broadly categorized as document types + * @param dataSourceIdMap a map of obj_id to datasource name + * @param filterByMediaMimeType match only on files whose mime types can be + * broadly categorized as media types + * @param filterByDocMimeType match only on files whose mime types can be + * broadly categorized as document types */ public AllIntraCaseCommonAttributeSearcher(Map dataSourceIdMap, boolean filterByMediaMimeType, boolean filterByDocMimeType, int percentageThreshold) { super(dataSourceIdMap, filterByMediaMimeType, filterByDocMimeType, percentageThreshold); @@ -44,14 +47,17 @@ final public class AllIntraCaseCommonAttributeSearcher extends IntraCaseCommonAt @Override protected String buildSqlSelectStatement() { - Object[] args = new String[] {SELECT_PREFIX, determineMimeTypeFilter()}; + Object[] args = new String[]{SELECT_PREFIX, determineMimeTypeFilter()}; return String.format(WHERE_CLAUSE, args); } + @NbBundle.Messages({ + "# {0} - build category", + "# {1} - threshold string", + "AllIntraCaseCommonAttributeSearcher.buildTabTitle.titleIntraAll=Common Properties (All Data Sources, {0}{1})" + }) @Override - String buildTabTitle() { - final String buildCategorySelectionString = this.buildCategorySelectionString(); - final String titleTemplate = Bundle.AbstractCommonFilesMetadataBuilder_buildTabTitle_titleIntraAll(); - return String.format(titleTemplate, new Object[]{buildCategorySelectionString}); + String getTabTitle() { + return Bundle.AllIntraCaseCommonAttributeSearcher_buildTabTitle_titleIntraAll(this.buildCategorySelectionString(), this.getPercentThresholdString()); } } diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/Bundle.properties b/Core/src/org/sleuthkit/autopsy/commonfilesearch/Bundle.properties index b8f2e3e0d7..1e6302d3d7 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/Bundle.properties @@ -1,30 +1,42 @@ CommonFilesPanel.commonFilesSearchLabel.text=Find files in multiple data sources in the current case. CommonFilesPanel.text=Indicate which data sources to consider while searching for duplicates: CommonFilesPanel.jRadioButton1.text=jRadioButton1 -CommonFilesPanel.jRadioButton2.text=With previous cases in the Central Repository +CommonFilesPanel.jRadioButton2.text=Between current case and cases in Central Repository CommonFilesPanel.intraCaseRadio.label=Correlate within current case only CommonFilesPanel.interCaseRadio.label=Correlate amongst all known cases (uses Central Repo) -IntraCasePanel.allDataSourcesRadioButton.text=Matches may be from any data source -IntraCasePanel.withinDataSourceRadioButton.text=At least one match must appear in the data source selected below: IntraCasePanel.selectDataSourceComboBox.actionCommand= -InterCasePanel.specificCentralRepoCaseRadio.text=Matches must be from the following Central Repo case: -InterCasePanel.anyCentralRepoCaseRadio.text=Matches may be from any Central Repo case CommonAttributePanel.jCheckBox1.text=Hide files found in over CommonAttributePanel.jLabel1.text=% of data sources in central repository. -CommonAttributePanel.percentageThresholdTextTwo.text_1=% of data sources in central repository. -CommonAttributePanel.percentageThresholdTextOne.text=20 CommonAttributePanel.percentageThresholdCheck.text_1=Hide files found in over -CommonAttributePanel.intraCaseRadio.text=Within current case -CommonAttributePanel.commonFilesSearchLabel1.text=Find common files to correlate data soures or cases. +CommonAttributePanel.jCheckBox1.text=Hide files found in over +CommonAttributePanel.jLabel1.text=% of data sources in central repository. +CommonAttributePanel.percentageThreshold.text=20 +CommonAttributePanel.jLabel1.text_1=% of data sources in central repository. +InterCasePanel.correlationTypeComboBox.toolTipText=Selected Correlation Type +IntraCasePanel.selectedFileCategoriesButton.text=Only the selected file types: +IntraCasePanel.categoriesLabel.text=File Types To Show: +IntraCasePanel.allFileCategoriesRadioButton.toolTipText=No filtering applied to results... +IntraCasePanel.allFileCategoriesRadioButton.text=All file types +IntraCasePanel.documentsCheckbox.text=Documents +IntraCasePanel.pictureVideoCheckbox.text=Pictures and Videos +IntraCasePanel.selectedFileCategoriesButton.toolTipText=Select from the options below... +CommonAttributePanel.percentageThresholdTextTwo.text_1=% of data sources in Central Repository. +CommonAttributePanel.percentageThresholdCheck.text_1_1=Hide items found in over +CommonAttributePanel.intraCaseRadio.text=Between data sources in current case CommonAttributePanel.errorText.text=In order to search, you must select a file category. -CommonAttributePanel.categoriesLabel.text=File Types To Include: -CommonAttributePanel.documentsCheckbox.text=Documents -CommonAttributePanel.pictureVideoCheckbox.text=Pictures and Videos -CommonAttributePanel.selectedFileCategoriesButton.toolTipText=Select from the options below... -CommonAttributePanel.selectedFileCategoriesButton.text=Only the selected file types: -CommonAttributePanel.allFileCategoriesRadioButton.toolTipText=No filtering applied to results... -CommonAttributePanel.allFileCategoriesRadioButton.text=All file types -CommonAttributePanel.cancelButton.actionCommand=Cancel -CommonAttributePanel.cancelButton.text=Cancel CommonAttributePanel.searchButton.text=Search -CommonAttributePanel.commonFilesSearchLabel2.text=Scope of Search +InterCasePanel.categoriesLabel.text=File Types To Show: +InterCasePanel.documentsCheckbox.text=Documents +InterCasePanel.pictureVideoCheckbox.text=Pictures and Videos +InterCasePanel.selectedFileCategoriesButton.toolTipText=Select from the options below... +InterCasePanel.selectedFileCategoriesButton.text=Only the selected file types: +InterCasePanel.allFileCategoriesRadioButton.toolTipText=No filtering applied to results... +InterCasePanel.allFileCategoriesRadioButton.text=All file types +InterCasePanel.specificCentralRepoCaseCheckbox.text=Common items need to exist in a specific case +IntraCasePanel.onlySpecificDataSourceCheckbox.text=Common items need to exist in a specific data source +CommonAttributePanel.interCasePanel.border.title=Central Repository Options +CommonAttributePanel.intraCasePanel.border.title=Current Case Options +CommonAttributePanel.commonItemSearchDescription.text=Find items that exist in multiple data sources or cases +CommonAttributePanel.scopeLabel.text=Scope of Search +InterCasePanel.correlationComboBoxLabel.text=Property Type To Match +CommonAttributePanel.percentageThresholdInputBox.text=20 diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CaseDBCommonAttributeInstance.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CaseDBCommonAttributeInstance.java index 9221a5dc86..7bf6801c6b 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CaseDBCommonAttributeInstance.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CaseDBCommonAttributeInstance.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.logging.Level; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; import org.sleuthkit.datamodel.AbstractFile; @@ -70,4 +71,10 @@ final public class CaseDBCommonAttributeInstance extends AbstractCommonAttribute return null; } } + + @Override + public CorrelationAttributeInstance.Type getCorrelationAttributeInstanceType() { + //may be required at a later date + throw new UnsupportedOperationException("Not supported yet."); + } } diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CaseDBCommonAttributeInstanceNode.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CaseDBCommonAttributeInstanceNode.java index f105d72498..45ba05735e 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CaseDBCommonAttributeInstanceNode.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CaseDBCommonAttributeInstanceNode.java @@ -18,28 +18,33 @@ */ package org.sleuthkit.autopsy.commonfilesearch; +import java.util.List; import org.apache.commons.lang3.StringUtils; import org.openide.nodes.Sheet; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor; import org.sleuthkit.autopsy.datamodel.FileNode; import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.ContentTag; /** - * Node that wraps CaseDBCommonAttributeInstance to represent a file instance stored - * in the CaseDB. + * Node that wraps CaseDBCommonAttributeInstance to represent a file instance + * stored in the CaseDB. */ public class CaseDBCommonAttributeInstanceNode extends FileNode { - + private final String caseName; private final String dataSource; /** * Create a node which can be used in a multilayer tree table and is based * on an AbstractFile. + * + * @param fsContent the file which is being represented by this node + * @param caseName the name of the case + * @param dataSource the datasource which contains the file * - * @param fsContent - * @param dataSource */ public CaseDBCommonAttributeInstanceNode(AbstractFile fsContent, String caseName, String dataSource) { super(fsContent); @@ -48,20 +53,20 @@ public class CaseDBCommonAttributeInstanceNode extends FileNode { } @Override - public boolean isLeafTypeNode(){ + public boolean isLeafTypeNode() { //Not used atm - could maybe be leveraged for better use in Children objects return true; } - + @Override public T accept(DisplayableItemNodeVisitor visitor) { return visitor.visit(this); } - public String getCase(){ + public String getCase() { return this.caseName; } - + public String getDataSource() { return this.dataSource; } @@ -74,18 +79,22 @@ public class CaseDBCommonAttributeInstanceNode extends FileNode { sheetSet = Sheet.createPropertiesSet(); sheet.put(sheetSet); } + List tags = getContentTagsFromDatabase(); final String NO_DESCR = Bundle.CommonFilesSearchResultsViewerTable_noDescText(); - + sheetSet.put(new NodeProperty<>(Bundle.CommonFilesSearchResultsViewerTable_filesColLbl(), Bundle.CommonFilesSearchResultsViewerTable_filesColLbl(), NO_DESCR, this.getContent().getName())); + CorrelationAttributeInstance correlationAttribute = getCorrelationAttributeInstance(); + this.addScoreProperty(sheetSet, tags); + this.addCommentProperty(sheetSet, tags, correlationAttribute); + this.addCountProperty(sheetSet, correlationAttribute); sheetSet.put(new NodeProperty<>(Bundle.CommonFilesSearchResultsViewerTable_pathColLbl(), Bundle.CommonFilesSearchResultsViewerTable_pathColLbl(), NO_DESCR, this.getContent().getParentPath())); sheetSet.put(new NodeProperty<>(Bundle.CommonFilesSearchResultsViewerTable_hashsetHitsColLbl(), Bundle.CommonFilesSearchResultsViewerTable_hashsetHitsColLbl(), NO_DESCR, getHashSetHitsCsvList(this.getContent()))); sheetSet.put(new NodeProperty<>(Bundle.CommonFilesSearchResultsViewerTable_dataSourceColLbl(), Bundle.CommonFilesSearchResultsViewerTable_dataSourceColLbl(), NO_DESCR, this.getDataSource())); sheetSet.put(new NodeProperty<>(Bundle.CommonFilesSearchResultsViewerTable_mimeTypeColLbl(), Bundle.CommonFilesSearchResultsViewerTable_mimeTypeColLbl(), NO_DESCR, StringUtils.defaultString(this.getContent().getMIMEType()))); + this.addTagProperty(sheetSet, tags); + sheetSet.put(new NodeProperty<>(Bundle.CommonFilesSearchResultsViewerTable_caseColLbl1(), Bundle.CommonFilesSearchResultsViewerTable_caseColLbl1(), NO_DESCR, caseName)); - - this.addTagProperty(sheetSet); - return sheet; } -} +} \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CentralRepoCommonAttributeInstance.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CentralRepoCommonAttributeInstance.java index 6cb58b9643..df6d24b0bb 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CentralRepoCommonAttributeInstance.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CentralRepoCommonAttributeInstance.java @@ -44,12 +44,19 @@ final public class CentralRepoCommonAttributeInstance extends AbstractCommonAttr private static final Logger LOGGER = Logger.getLogger(CentralRepoCommonAttributeInstance.class.getName()); private final Integer crFileId; private CorrelationAttributeInstance currentAttribute; + private final CorrelationAttributeInstance.Type correlationType; private final Map dataSourceNameToIdMap; - CentralRepoCommonAttributeInstance(Integer attrInstId, Map dataSourceIdToNameMap) { + CentralRepoCommonAttributeInstance(Integer attrInstId, Map dataSourceIdToNameMap, CorrelationAttributeInstance.Type correlationType) { super(); this.crFileId = attrInstId; this.dataSourceNameToIdMap = invertMap(dataSourceIdToNameMap); + this.correlationType = correlationType; + } + + @Override + public CorrelationAttributeInstance.Type getCorrelationAttributeInstanceType(){ + return this.correlationType; } void setCurrentAttributeInst(CorrelationAttributeInstance attribute) { @@ -61,16 +68,15 @@ final public class CentralRepoCommonAttributeInstance extends AbstractCommonAttr Case currentCase; if (this.currentAttribute != null) { - + final CorrelationAttributeInstance currentAttributeInstance = this.currentAttribute; - + String currentFullPath = currentAttributeInstance.getFilePath(); String currentDataSource = currentAttributeInstance.getCorrelationDataSource().getName(); - - - if(this.dataSourceNameToIdMap.containsKey(currentDataSource)){ + + if (this.dataSourceNameToIdMap.containsKey(currentDataSource)) { Long dataSourceObjectId = this.dataSourceNameToIdMap.get(currentDataSource); - + try { currentCase = Case.getCurrentCaseThrows(); @@ -83,9 +89,9 @@ final public class CentralRepoCommonAttributeInstance extends AbstractCommonAttr final String whereClause = String.format("lower(name) = '%s' AND md5 = '%s' AND lower(parent_path) = '%s' AND data_source_obj_id = %s", fileName, currentAttribute.getCorrelationValue(), parentPath, dataSourceObjectId); List potentialAbstractFiles = tskDb.findAllFilesWhere(whereClause); - if(potentialAbstractFiles.isEmpty()){ + if (potentialAbstractFiles.isEmpty()) { return null; - } else if(potentialAbstractFiles.size() > 1){ + } else if (potentialAbstractFiles.size() > 1) { LOGGER.log(Level.WARNING, String.format("Unable to find an exact match for AbstractFile for record with filePath: %s. May have returned the wrong file.", new Object[]{currentFullPath})); return potentialAbstractFiles.get(0); } else { @@ -98,7 +104,7 @@ final public class CentralRepoCommonAttributeInstance extends AbstractCommonAttr } } else { return null; - } + } } return null; } @@ -107,7 +113,7 @@ final public class CentralRepoCommonAttributeInstance extends AbstractCommonAttr public DisplayableItemNode[] generateNodes() { // @@@ We should be doing more of this work in teh generateKeys method. We want to do as little as possible in generateNodes - InterCaseSearchResultsProcessor eamDbAttrInst = new InterCaseSearchResultsProcessor(); + InterCaseSearchResultsProcessor eamDbAttrInst = new InterCaseSearchResultsProcessor(correlationType); CorrelationAttributeInstance corrAttr = eamDbAttrInst.findSingleCorrelationAttribute(crFileId); List attrInstNodeList = new ArrayList<>(0); String currCaseDbName = Case.getCurrentCase().getDisplayName(); @@ -128,7 +134,7 @@ final public class CentralRepoCommonAttributeInstance extends AbstractCommonAttr private Map invertMap(Map dataSourceIdToNameMap) { HashMap invertedMap = new HashMap<>(); - for (Map.Entry entry : dataSourceIdToNameMap.entrySet()){ + for (Map.Entry entry : dataSourceIdToNameMap.entrySet()) { invertedMap.put(entry.getValue(), entry.getKey()); } return invertedMap; diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributePanel.form b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributePanel.form index 20cbb33fe9..5f0beb57e0 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributePanel.form +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributePanel.form @@ -2,17 +2,12 @@
- - - - - - + @@ -32,7 +27,7 @@ - + @@ -40,13 +35,13 @@ - + - + - + @@ -59,55 +54,49 @@ - + + + - + + + + + + + - - - - - - - - - + + + - - - - - - - + + - - - - + + + + - - - - - - - - - - - + + + - + + + + + + @@ -117,42 +106,29 @@ - - - + + + - - - - - - - - - - - + + + - + - - - - - - - - - - + + + + + @@ -160,10 +136,10 @@ - + - + @@ -180,83 +156,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -268,10 +167,10 @@ - + - + @@ -303,40 +202,21 @@ - - - - - - - - - - - - - - - - - - - - - + - + + - + @@ -356,25 +236,48 @@ - + - - + + + + + + + + + + + + + + + + - - - - + + + + + + + + - + + + + + + + - - - + + diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributePanel.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributePanel.java index 21f2b72624..c4aa8019f2 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributePanel.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributePanel.java @@ -25,6 +25,9 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Observable; +import java.util.Observer; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import javax.swing.JFrame; @@ -38,6 +41,7 @@ import org.openide.util.NbBundle; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationCase; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; @@ -55,7 +59,7 @@ import org.sleuthkit.datamodel.TskCoreException; * logic. Nested within CommonFilesDialog. */ @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives -public final class CommonAttributePanel extends javax.swing.JDialog { +final class CommonAttributePanel extends javax.swing.JDialog implements Observer { private static final Logger LOGGER = Logger.getLogger(CommonAttributePanel.class.getName()); private static final long serialVersionUID = 1L; @@ -64,50 +68,44 @@ public final class CommonAttributePanel extends javax.swing.JDialog { private final UserInputErrorManager errorManager; - private boolean pictureViewCheckboxState; - - private boolean documentsCheckboxState; - private int percentageThresholdValue = 20; /** * Creates new form CommonFilesPanel */ @NbBundle.Messages({ - "CommonFilesPanel.title=Common Files Panel", - "CommonFilesPanel.exception=Unexpected Exception loading DataSources.", - "CommonFilesPanel.frame.title=Find Common Files", - "CommonFilesPanel.frame.msg=Find Common Files"}) - public CommonAttributePanel() { - super(new JFrame(Bundle.CommonFilesPanel_frame_title()), - Bundle.CommonFilesPanel_frame_msg(), true); + "CommonAttributePanel.title=Common Property Panel", + "CommonAttributePanel.exception=Unexpected Exception loading DataSources.", + "CommonAttributePanel.frame.title=Find Common Properties", + "CommonAttributePanel.frame.msg=Find Common Properties", + "CommonAttributePanel.intraCasePanel.title=Curren Case Options"}) + CommonAttributePanel() { + super(new JFrame(Bundle.CommonAttributePanel_frame_title()), + Bundle.CommonAttributePanel_frame_msg(), true); initComponents(); - this.setLocationRelativeTo(WindowManager.getDefault().getMainWindow()); this.setupDataSources(); - + intraCasePanel.setVisible(true); + interCasePanel.setVisible(false); if (CommonAttributePanel.isEamDbAvailableForIntercaseSearch()) { this.setupCases(); + this.interCasePanel.setupCorrelationTypeFilter(); } else { this.disableIntercaseSearch(); } - - if(CommonAttributePanel.isEamDbAvailableForPercentageFrequencyCalculations()){ - this.enablePercentageOptions(); - } else { - this.disablePercentageOptions(); - } + + this.updatePercentageOptions(CommonAttributePanel.getNumberOfDataSourcesAvailable()); this.errorManager = new UserInputErrorManager(); - - this.percentageThresholdTextOne.getDocument().addDocumentListener(new DocumentListener(){ - - private Dimension preferredSize = CommonAttributePanel.this.percentageThresholdTextOne.getPreferredSize(); - - private void maintainSize(){ - CommonAttributePanel.this.percentageThresholdTextOne.setSize(preferredSize); + + this.percentageThresholdInputBox.getDocument().addDocumentListener(new DocumentListener() { + + private final Dimension preferredSize = CommonAttributePanel.this.percentageThresholdInputBox.getPreferredSize(); + + private void maintainSize() { + CommonAttributePanel.this.percentageThresholdInputBox.setSize(preferredSize); } - + @Override public void insertUpdate(DocumentEvent event) { this.maintainSize(); @@ -128,7 +126,14 @@ public final class CommonAttributePanel extends javax.swing.JDialog { }); } - private static boolean isEamDbAvailableForIntercaseSearch() { + /** + * Get whether or not the central repository will be enabled as a search + * option. + * + * @return true if the central repository exists and has at least 2 cases in + * and includes the current case, false otherwise. + */ + static boolean isEamDbAvailableForIntercaseSearch() { try { return EamDb.isEnabled() && EamDb.getInstance() != null @@ -141,57 +146,62 @@ public final class CommonAttributePanel extends javax.swing.JDialog { } return false; } - - private static boolean isEamDbAvailableForPercentageFrequencyCalculations(){ - try { - return EamDb.isEnabled() - && EamDb.getInstance() != null - && EamDb.getInstance().getCases().size() > 0; + + @Override + public void update(Observable o, Object arg) { + checkFileTypeCheckBoxState(); + } + + /** + * Get the number of data sources in the central repository if it is + * enabled, zero if it is not enabled. + * + * @return the number of data sources in the current central repo, or 0 if + * it is disabled + */ + private static Long getNumberOfDataSourcesAvailable() { + try { + if (EamDb.isEnabled() + && EamDb.getInstance() != null) { + return EamDb.getInstance().getCountUniqueDataSources(); + } } catch (EamDbException ex) { LOGGER.log(Level.SEVERE, "Unexpected exception while checking for EamDB enabled.", ex); } - return false; + return 0L; } + /** + * Disable the option to search for common attributes in the central + * repository. + */ private void disableIntercaseSearch() { this.intraCaseRadio.setSelected(true); this.interCaseRadio.setEnabled(false); } + /** + * Perform the common attribute search. + */ @NbBundle.Messages({ - "CommonFilesPanel.search.results.titleAll=Common Files (All Data Sources)", - "CommonFilesPanel.search.results.titleSingle=Common Files (Match Within Data Source: %s)", - "CommonFilesPanel.search.results.pathText=Common Files Search Results", - "CommonFilesPanel.search.done.searchProgressGathering=Gathering Common Files Search Results.", - "CommonFilesPanel.search.done.searchProgressDisplay=Displaying Common Files Search Results.", - "CommonFilesPanel.search.done.tskCoreException=Unable to run query against DB.", - "CommonFilesPanel.search.done.noCurrentCaseException=Unable to open case file.", - "CommonFilesPanel.search.done.exception=Unexpected exception running Common Files Search.", - "CommonFilesPanel.search.done.interupted=Something went wrong finding common files.", - "CommonFilesPanel.search.done.sqlException=Unable to query db for files or data sources."}) + "CommonAttributePanel.search.results.pathText=Common Property Search Results", + "CommonAttributePanel.search.done.searchProgressGathering=Gathering Common Property Search Results.", + "CommonAttributePanel.search.done.searchProgressDisplay=Displaying Common Property Search Results.", + "CommonAttributePanel.search.done.tskCoreException=Unable to run query against DB.", + "CommonAttributePanel.search.done.noCurrentCaseException=Unable to open case file.", + "CommonAttributePanel.search.done.exception=Unexpected exception running Common Property Search.", + "CommonAttributePanel.search.done.interupted=Something went wrong finding common properties.", + "CommonAttributePanel.search.done.sqlException=Unable to query db for properties or data sources."}) private void search() { - String pathText = Bundle.CommonFilesPanel_search_results_pathText(); new SwingWorker() { private String tabTitle; private ProgressHandle progress; - private void setTitleForAllDataSources() { - this.tabTitle = Bundle.CommonFilesPanel_search_results_titleAll(); - } - - private void setTitleForSingleSource(Long dataSourceId) { - final String CommonFilesPanel_search_results_titleSingle = Bundle.CommonFilesPanel_search_results_titleSingle(); - final Object[] dataSourceName = new Object[]{intraCasePanel.getDataSourceMap().get(dataSourceId)}; - - this.tabTitle = String.format(CommonFilesPanel_search_results_titleSingle, dataSourceName); - } - @Override - @SuppressWarnings({"BoxedValueEquality", "NumberEquality"}) protected CommonAttributeSearchResults doInBackground() throws TskCoreException, NoCurrentCaseException, SQLException, EamDbException { - progress = ProgressHandle.createHandle(Bundle.CommonFilesPanel_search_done_searchProgressGathering()); + progress = ProgressHandle.createHandle(Bundle.CommonAttributePanel_search_done_searchProgressGathering()); progress.start(); progress.switchToIndeterminate(); @@ -203,42 +213,44 @@ public final class CommonAttributePanel extends javax.swing.JDialog { boolean filterByMedia = false; boolean filterByDocuments = false; - if (selectedFileCategoriesButton.isSelected()) { - if (pictureVideoCheckbox.isSelected()) { - filterByMedia = true; - } - if (documentsCheckbox.isSelected()) { - filterByDocuments = true; - } - } int percentageThreshold = CommonAttributePanel.this.percentageThresholdValue; - + if (!CommonAttributePanel.this.percentageThresholdCheck.isSelected()) { //0 has the effect of disabling the feature percentageThreshold = 0; } if (CommonAttributePanel.this.interCaseRadio.isSelected()) { - - if (caseId == InterCasePanel.NO_CASE_SELECTED) { - builder = new AllInterCaseCommonAttributeSearcher(intraCasePanel.getDataSourceMap(), filterByMedia, filterByDocuments, percentageThreshold); - } else { - builder = new SingleInterCaseCommonAttributeSearcher(caseId, intraCasePanel.getDataSourceMap(), filterByMedia, filterByDocuments, percentageThreshold); + CorrelationAttributeInstance.Type corType = interCasePanel.getSelectedCorrelationType(); + if (interCasePanel.fileCategoriesButtonIsSelected()) { + filterByMedia = interCasePanel.pictureVideoCheckboxIsSelected(); + filterByDocuments = interCasePanel.documentsCheckboxIsSelected(); } - } else { - if (dataSourceId == CommonAttributePanel.NO_DATA_SOURCE_SELECTED) { - builder = new AllIntraCaseCommonAttributeSearcher(intraCasePanel.getDataSourceMap(), filterByMedia, filterByDocuments, percentageThreshold); + if (corType == null) { + corType = CorrelationAttributeInstance.getDefaultCorrelationTypes().get(0); + } + if (caseId == InterCasePanel.NO_CASE_SELECTED) { + builder = new AllInterCaseCommonAttributeSearcher(intraCasePanel.getDataSourceMap(), filterByMedia, filterByDocuments, corType, percentageThreshold); + } else { - setTitleForAllDataSources(); + builder = new SingleInterCaseCommonAttributeSearcher(caseId, intraCasePanel.getDataSourceMap(), filterByMedia, filterByDocuments, corType, percentageThreshold); + } + + } else { + if (intraCasePanel.fileCategoriesButtonIsSelected()) { + filterByMedia = intraCasePanel.pictureVideoCheckboxIsSelected(); + filterByDocuments = intraCasePanel.documentsCheckboxIsSelected(); + } + if (Objects.equals(dataSourceId, CommonAttributePanel.NO_DATA_SOURCE_SELECTED)) { + builder = new AllIntraCaseCommonAttributeSearcher(intraCasePanel.getDataSourceMap(), filterByMedia, filterByDocuments, percentageThreshold); } else { builder = new SingleIntraCaseCommonAttributeSearcher(dataSourceId, intraCasePanel.getDataSourceMap(), filterByMedia, filterByDocuments, percentageThreshold); - - setTitleForSingleSource(dataSourceId); } + } - metadata = builder.findFiles(); - this.tabTitle = builder.buildTabTitle(); + metadata = builder.findMatches(); + this.tabTitle = builder.getTabTitle(); return metadata; } @@ -262,28 +274,28 @@ public final class CommonAttributePanel extends javax.swing.JDialog { Collection viewers = new ArrayList<>(1); viewers.add(table); - progress.setDisplayName(Bundle.CommonFilesPanel_search_done_searchProgressDisplay()); - DataResultTopComponent.createInstance(tabTitle, pathText, tableFilterWithDescendantsNode, metadata.size(), viewers); + progress.setDisplayName(Bundle.CommonAttributePanel_search_done_searchProgressDisplay()); + DataResultTopComponent.createInstance(tabTitle, Bundle.CommonAttributePanel_search_results_pathText(), tableFilterWithDescendantsNode, metadata.size(), viewers); progress.finish(); } catch (InterruptedException ex) { LOGGER.log(Level.SEVERE, "Interrupted while loading Common Files", ex); - MessageNotifyUtil.Message.error(Bundle.CommonFilesPanel_search_done_interupted()); + MessageNotifyUtil.Message.error(Bundle.CommonAttributePanel_search_done_interupted()); } catch (ExecutionException ex) { String errorMessage; Throwable inner = ex.getCause(); if (inner instanceof TskCoreException) { LOGGER.log(Level.SEVERE, "Failed to load files from database.", ex); - errorMessage = Bundle.CommonFilesPanel_search_done_tskCoreException(); + errorMessage = Bundle.CommonAttributePanel_search_done_tskCoreException(); } else if (inner instanceof NoCurrentCaseException) { LOGGER.log(Level.SEVERE, "Current case has been closed.", ex); - errorMessage = Bundle.CommonFilesPanel_search_done_noCurrentCaseException(); + errorMessage = Bundle.CommonAttributePanel_search_done_noCurrentCaseException(); } else if (inner instanceof SQLException) { LOGGER.log(Level.SEVERE, "Unable to query db for files.", ex); - errorMessage = Bundle.CommonFilesPanel_search_done_sqlException(); + errorMessage = Bundle.CommonAttributePanel_search_done_sqlException(); } else { LOGGER.log(Level.SEVERE, "Unexpected exception while running Common Files Search.", ex); - errorMessage = Bundle.CommonFilesPanel_search_done_exception(); + errorMessage = Bundle.CommonAttributePanel_search_done_exception(); } MessageNotifyUtil.Message.error(errorMessage); } @@ -296,19 +308,23 @@ public final class CommonAttributePanel extends javax.swing.JDialog { * future usage. * * @return a mapping of data correlationCase ids to data correlationCase - * names + * names */ @NbBundle.Messages({ - "CommonFilesPanel.setupDataSources.done.tskCoreException=Unable to run query against DB.", - "CommonFilesPanel.setupDataSources.done.noCurrentCaseException=Unable to open case file.", - "CommonFilesPanel.setupDataSources.done.exception=Unexpected exception loading data sources.", - "CommonFilesPanel.setupDataSources.done.interupted=Something went wrong building the Common Files Search dialog box.", - "CommonFilesPanel.setupDataSources.done.sqlException=Unable to query db for data sources.", - "CommonFilesPanel.setupDataSources.updateUi.noDataSources=No data sources were found."}) + "CommonAttributePanel.setupDataSources.done.tskCoreException=Unable to run query against DB.", + "CommonAttributePanel.setupDataSources.done.noCurrentCaseException=Unable to open case file.", + "CommonAttributePanel.setupDataSources.done.exception=Unexpected exception loading data sources.", + "CommonAttributePanel.setupDataSources.done.interupted=Something went wrong building the Common Files Search dialog box.", + "CommonAttributePanel.setupDataSources.done.sqlException=Unable to query db for data sources.", + "CommonAttributePanel.setupDataSources.updateUi.noDataSources=No data sources were found."}) private void setupDataSources() { new SwingWorker, Void>() { + /** + * Update the user interface of the panel to reflect the datasources + * available. + */ private void updateUi() { final Map dataSourceMap = CommonAttributePanel.this.intraCasePanel.getDataSourceMap(); @@ -318,17 +334,26 @@ public final class CommonAttributePanel extends javax.swing.JDialog { //only enable all this stuff if we actually have datasources if (dataSourcesNames.length > 0) { dataSourcesNames = dataSourceMap.values().toArray(dataSourcesNames); - CommonAttributePanel.this.intraCasePanel.setDataModel(new DataSourceComboBoxModel(dataSourcesNames)); + CommonAttributePanel.this.intraCasePanel.setDatasourceComboboxModel(new DataSourceComboBoxModel(dataSourcesNames)); - boolean multipleDataSources = this.caseHasMultipleSources(); - CommonAttributePanel.this.intraCasePanel.rigForMultipleDataSources(multipleDataSources); - - CommonAttributePanel.this.updateErrorTextAndSearchBox(); + if (!this.caseHasMultipleSources()) { //disable intra case search when only 1 data source in current case + intraCaseRadio.setEnabled(false); + interCaseRadio.setSelected(true); + intraCasePanel.setVisible(false); + interCasePanel.setVisible(true); + } + CommonAttributePanel.this.updateErrorTextAndSearchButton(); } } + /** + * Check if the case has multiple data sources + * + * @return true if the case has multiple data sources, false + * otherwise + */ private boolean caseHasMultipleSources() { - return CommonAttributePanel.this.intraCasePanel.getDataSourceMap().size() > 2; + return CommonAttributePanel.this.intraCasePanel.getDataSourceMap().size() > 1; } @Override @@ -346,22 +371,22 @@ public final class CommonAttributePanel extends javax.swing.JDialog { } catch (InterruptedException ex) { LOGGER.log(Level.SEVERE, "Interrupted while building Common Files Search dialog.", ex); - MessageNotifyUtil.Message.error(Bundle.CommonFilesPanel_setupDataSources_done_interupted()); + MessageNotifyUtil.Message.error(Bundle.CommonAttributePanel_setupDataSources_done_interupted()); } catch (ExecutionException ex) { String errorMessage; Throwable inner = ex.getCause(); if (inner instanceof TskCoreException) { LOGGER.log(Level.SEVERE, "Failed to load data sources from database.", ex); - errorMessage = Bundle.CommonFilesPanel_setupDataSources_done_tskCoreException(); + errorMessage = Bundle.CommonAttributePanel_setupDataSources_done_tskCoreException(); } else if (inner instanceof NoCurrentCaseException) { LOGGER.log(Level.SEVERE, "Current case has been closed.", ex); - errorMessage = Bundle.CommonFilesPanel_setupDataSources_done_noCurrentCaseException(); + errorMessage = Bundle.CommonAttributePanel_setupDataSources_done_noCurrentCaseException(); } else if (inner instanceof SQLException) { LOGGER.log(Level.SEVERE, "Unable to query db for data sources.", ex); - errorMessage = Bundle.CommonFilesPanel_setupDataSources_done_sqlException(); + errorMessage = Bundle.CommonAttributePanel_setupDataSources_done_sqlException(); } else { LOGGER.log(Level.SEVERE, "Unexpected exception while building Common Files Search dialog panel.", ex); - errorMessage = Bundle.CommonFilesPanel_setupDataSources_done_exception(); + errorMessage = Bundle.CommonAttributePanel_setupDataSources_done_exception(); } MessageNotifyUtil.Message.error(errorMessage); } @@ -370,12 +395,16 @@ public final class CommonAttributePanel extends javax.swing.JDialog { } @NbBundle.Messages({ - "CommonFilesPanel.setupCases.done.interruptedException=Something went wrong building the Common Files Search dialog box.", - "CommonFilesPanel.setupCases.done.exeutionException=Unexpected exception loading cases."}) + "CommonAttributePanel.setupCases.done.interruptedException=Something went wrong building the Common Files Search dialog box.", + "CommonAttributePanel.setupCases.done.exeutionException=Unexpected exception loading cases."}) private void setupCases() { new SwingWorker, Void>() { + /** + * Update the user interface of the panel to reflect the cases + * available. + */ private void updateUi() { final Map caseMap = CommonAttributePanel.this.interCasePanel.getCaseMap(); @@ -384,17 +413,22 @@ public final class CommonAttributePanel extends javax.swing.JDialog { if (caseNames.length > 0) { caseNames = caseMap.values().toArray(caseNames); - CommonAttributePanel.this.interCasePanel.setCaseList(new DataSourceComboBoxModel(caseNames)); - - boolean multipleCases = this.centralRepoHasMultipleCases(); - CommonAttributePanel.this.interCasePanel.rigForMultipleCases(multipleCases); - + CommonAttributePanel.this.interCasePanel.setCaseComboboxModel(new DataSourceComboBoxModel(caseNames)); } else { CommonAttributePanel.this.disableIntercaseSearch(); } } - private Map mapDataSources(List cases) throws EamDbException { + /** + * Create a map of cases from a list of cases. + * + * @param cases + * + * @return a map of Cases + * + * @throws EamDbException + */ + private Map mapCases(List cases) throws EamDbException { Map casemap = new HashMap<>(); CorrelationCase currentCorCase = EamDb.getInstance().getCase(Case.getCurrentCase()); for (CorrelationCase correlationCase : cases) { @@ -402,7 +436,6 @@ public final class CommonAttributePanel extends javax.swing.JDialog { casemap.put(correlationCase.getID(), correlationCase.getDisplayName()); } } - return casemap; } @@ -410,7 +443,7 @@ public final class CommonAttributePanel extends javax.swing.JDialog { protected Map doInBackground() throws EamDbException { List dataSources = EamDb.getInstance().getCases(); - Map caseMap = mapDataSources(dataSources); + Map caseMap = mapCases(dataSources); return caseMap; } @@ -423,17 +456,13 @@ public final class CommonAttributePanel extends javax.swing.JDialog { this.updateUi(); } catch (InterruptedException ex) { LOGGER.log(Level.SEVERE, "Interrupted while building Common Files Search dialog.", ex); - MessageNotifyUtil.Message.error(Bundle.CommonFilesPanel_setupCases_done_interruptedException()); + MessageNotifyUtil.Message.error(Bundle.CommonAttributePanel_setupCases_done_interruptedException()); } catch (ExecutionException ex) { LOGGER.log(Level.SEVERE, "Unexpected exception while building Common Files Search dialog.", ex); - MessageNotifyUtil.Message.error(Bundle.CommonFilesPanel_setupCases_done_exeutionException()); + MessageNotifyUtil.Message.error(Bundle.CommonAttributePanel_setupCases_done_exeutionException()); } } - private boolean centralRepoHasMultipleCases() { - return CommonAttributePanel.this.interCasePanel.centralRepoHasMultipleCases(); - } - }.execute(); } @@ -446,32 +475,22 @@ public final class CommonAttributePanel extends javax.swing.JDialog { // //GEN-BEGIN:initComponents private void initComponents() { - fileTypeFilterButtonGroup = new javax.swing.ButtonGroup(); interIntraButtonGroup = new javax.swing.ButtonGroup(); jPanel1 = new javax.swing.JPanel(); - commonFilesSearchLabel2 = new javax.swing.JLabel(); + scopeLabel = new javax.swing.JLabel(); searchButton = new javax.swing.JButton(); - cancelButton = new javax.swing.JButton(); - allFileCategoriesRadioButton = new javax.swing.JRadioButton(); - selectedFileCategoriesButton = new javax.swing.JRadioButton(); - pictureVideoCheckbox = new javax.swing.JCheckBox(); - documentsCheckbox = new javax.swing.JCheckBox(); - categoriesLabel = new javax.swing.JLabel(); errorText = new javax.swing.JLabel(); - commonFilesSearchLabel1 = new javax.swing.JLabel(); + commonItemSearchDescription = new javax.swing.JLabel(); intraCaseRadio = new javax.swing.JRadioButton(); interCaseRadio = new javax.swing.JRadioButton(); - layoutPanel = new java.awt.Panel(); + percentageThresholdCheck = new javax.swing.JCheckBox(); + percentageThresholdInputBox = new javax.swing.JTextField(); + percentageThresholdTextTwo = new javax.swing.JLabel(); intraCasePanel = new org.sleuthkit.autopsy.commonfilesearch.IntraCasePanel(); interCasePanel = new org.sleuthkit.autopsy.commonfilesearch.InterCasePanel(); - percentageThresholdCheck = new javax.swing.JCheckBox(); - percentageThresholdTextOne = new javax.swing.JTextField(); - percentageThresholdTextTwo = new javax.swing.JLabel(); - filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 32767)); - filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 32767)); + dataSourcesLabel = new javax.swing.JLabel(); - setMaximumSize(new java.awt.Dimension(450, 375)); - setMinimumSize(new java.awt.Dimension(450, 375)); + setMinimumSize(new java.awt.Dimension(450, 460)); setResizable(false); addWindowListener(new java.awt.event.WindowAdapter() { public void windowClosed(java.awt.event.WindowEvent evt) { @@ -479,13 +498,13 @@ public final class CommonAttributePanel extends javax.swing.JDialog { } }); - jPanel1.setMaximumSize(new java.awt.Dimension(450, 375)); - jPanel1.setMinimumSize(new java.awt.Dimension(450, 375)); - jPanel1.setPreferredSize(new java.awt.Dimension(450, 375)); + jPanel1.setMaximumSize(new java.awt.Dimension(450, 460)); + jPanel1.setMinimumSize(new java.awt.Dimension(450, 460)); + jPanel1.setPreferredSize(new java.awt.Dimension(450, 460)); jPanel1.setRequestFocusEnabled(false); - org.openide.awt.Mnemonics.setLocalizedText(commonFilesSearchLabel2, org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.commonFilesSearchLabel2.text")); // NOI18N - commonFilesSearchLabel2.setFocusable(false); + org.openide.awt.Mnemonics.setLocalizedText(scopeLabel, org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.scopeLabel.text")); // NOI18N + scopeLabel.setFocusable(false); org.openide.awt.Mnemonics.setLocalizedText(searchButton, org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.searchButton.text")); // NOI18N searchButton.setEnabled(false); @@ -496,59 +515,12 @@ public final class CommonAttributePanel extends javax.swing.JDialog { } }); - org.openide.awt.Mnemonics.setLocalizedText(cancelButton, org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.cancelButton.text")); // NOI18N - cancelButton.setActionCommand(org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.cancelButton.actionCommand")); // NOI18N - cancelButton.setHorizontalTextPosition(javax.swing.SwingConstants.LEADING); - cancelButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - cancelButtonActionPerformed(evt); - } - }); - - fileTypeFilterButtonGroup.add(allFileCategoriesRadioButton); - org.openide.awt.Mnemonics.setLocalizedText(allFileCategoriesRadioButton, org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.allFileCategoriesRadioButton.text")); // NOI18N - allFileCategoriesRadioButton.setToolTipText(org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.allFileCategoriesRadioButton.toolTipText")); // NOI18N - allFileCategoriesRadioButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - allFileCategoriesRadioButtonActionPerformed(evt); - } - }); - - fileTypeFilterButtonGroup.add(selectedFileCategoriesButton); - selectedFileCategoriesButton.setSelected(true); - org.openide.awt.Mnemonics.setLocalizedText(selectedFileCategoriesButton, org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.selectedFileCategoriesButton.text")); // NOI18N - selectedFileCategoriesButton.setToolTipText(org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.selectedFileCategoriesButton.toolTipText")); // NOI18N - selectedFileCategoriesButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - selectedFileCategoriesButtonActionPerformed(evt); - } - }); - - pictureVideoCheckbox.setSelected(true); - org.openide.awt.Mnemonics.setLocalizedText(pictureVideoCheckbox, org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.pictureVideoCheckbox.text")); // NOI18N - pictureVideoCheckbox.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - pictureVideoCheckboxActionPerformed(evt); - } - }); - - documentsCheckbox.setSelected(true); - org.openide.awt.Mnemonics.setLocalizedText(documentsCheckbox, org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.documentsCheckbox.text")); // NOI18N - documentsCheckbox.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - documentsCheckboxActionPerformed(evt); - } - }); - - org.openide.awt.Mnemonics.setLocalizedText(categoriesLabel, org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.categoriesLabel.text")); // NOI18N - categoriesLabel.setName(""); // NOI18N - errorText.setForeground(new java.awt.Color(255, 0, 0)); org.openide.awt.Mnemonics.setLocalizedText(errorText, org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.errorText.text")); // NOI18N errorText.setVerticalAlignment(javax.swing.SwingConstants.TOP); - org.openide.awt.Mnemonics.setLocalizedText(commonFilesSearchLabel1, org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.commonFilesSearchLabel1.text")); // NOI18N - commonFilesSearchLabel1.setFocusable(false); + org.openide.awt.Mnemonics.setLocalizedText(commonItemSearchDescription, org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.commonItemSearchDescription.text")); // NOI18N + commonItemSearchDescription.setFocusable(false); interIntraButtonGroup.add(intraCaseRadio); intraCaseRadio.setSelected(true); @@ -567,24 +539,32 @@ public final class CommonAttributePanel extends javax.swing.JDialog { } }); - layoutPanel.setLayout(new java.awt.CardLayout()); - layoutPanel.add(intraCasePanel, "card3"); - layoutPanel.add(interCasePanel, "card2"); - - org.openide.awt.Mnemonics.setLocalizedText(percentageThresholdCheck, org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.percentageThresholdCheck.text_1")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(percentageThresholdCheck, org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.percentageThresholdCheck.text_1_1")); // NOI18N percentageThresholdCheck.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { percentageThresholdCheckActionPerformed(evt); } }); - percentageThresholdTextOne.setText(org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.percentageThresholdTextOne.text")); // NOI18N - percentageThresholdTextOne.setMaximumSize(new java.awt.Dimension(40, 24)); - percentageThresholdTextOne.setMinimumSize(new java.awt.Dimension(40, 24)); - percentageThresholdTextOne.setPreferredSize(new java.awt.Dimension(40, 24)); + percentageThresholdInputBox.setHorizontalAlignment(javax.swing.JTextField.TRAILING); + percentageThresholdInputBox.setText(org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.percentageThresholdInputBox.text")); // NOI18N + percentageThresholdInputBox.setMaximumSize(new java.awt.Dimension(40, 24)); + percentageThresholdInputBox.setMinimumSize(new java.awt.Dimension(40, 24)); + percentageThresholdInputBox.setPreferredSize(new java.awt.Dimension(40, 24)); org.openide.awt.Mnemonics.setLocalizedText(percentageThresholdTextTwo, org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.percentageThresholdTextTwo.text_1")); // NOI18N + intraCasePanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.intraCasePanel.border.title"))); // NOI18N + intraCasePanel.setMaximumSize(new java.awt.Dimension(32779, 192)); + intraCasePanel.setMinimumSize(new java.awt.Dimension(204, 192)); + intraCasePanel.setPreferredSize(new java.awt.Dimension(430, 192)); + intraCasePanel.setVerifyInputWhenFocusTarget(false); + + interCasePanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(CommonAttributePanel.class, "CommonAttributePanel.interCasePanel.border.title"))); // NOI18N + interCasePanel.setMaximumSize(new java.awt.Dimension(32779, 230)); + interCasePanel.setMinimumSize(new java.awt.Dimension(430, 230)); + interCasePanel.setPreferredSize(new java.awt.Dimension(430, 230)); + javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1); jPanel1.setLayout(jPanel1Layout); jPanel1Layout.setHorizontalGroup( @@ -593,80 +573,63 @@ public final class CommonAttributePanel extends javax.swing.JDialog { .addContainerGap() .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() + .addComponent(errorText, javax.swing.GroupLayout.DEFAULT_SIZE, 300, Short.MAX_VALUE) + .addGap(65, 65, 65) .addComponent(searchButton) + .addContainerGap()) + .addGroup(jPanel1Layout.createSequentialGroup() + .addComponent(scopeLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGap(37, 37, 37)) + .addGroup(jPanel1Layout.createSequentialGroup() + .addComponent(percentageThresholdCheck) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(cancelButton) + .addComponent(percentageThresholdInputBox, javax.swing.GroupLayout.PREFERRED_SIZE, 40, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(jPanel1Layout.createSequentialGroup() - .addGap(0, 0, Short.MAX_VALUE) - .addComponent(filler1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(80, 80, 80) - .addComponent(filler2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addComponent(errorText))) + .addComponent(percentageThresholdTextTwo, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) .addGroup(jPanel1Layout.createSequentialGroup() .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(commonFilesSearchLabel2) - .addComponent(intraCaseRadio) - .addComponent(interCaseRadio) - .addComponent(commonFilesSearchLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(categoriesLabel) - .addComponent(selectedFileCategoriesButton) - .addGroup(jPanel1Layout.createSequentialGroup() - .addGap(29, 29, 29) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(documentsCheckbox) - .addComponent(pictureVideoCheckbox))) - .addComponent(allFileCategoriesRadioButton) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() - .addGap(10, 10, 10) - .addComponent(layoutPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) - .addGroup(jPanel1Layout.createSequentialGroup() - .addComponent(percentageThresholdCheck) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(percentageThresholdTextOne, javax.swing.GroupLayout.PREFERRED_SIZE, 40, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(percentageThresholdTextTwo))) - .addContainerGap(9, Short.MAX_VALUE)))) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) + .addComponent(commonItemSearchDescription, javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.LEADING, jPanel1Layout.createSequentialGroup() + .addGap(20, 20, 20) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(intraCaseRadio, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(interCaseRadio, javax.swing.GroupLayout.DEFAULT_SIZE, 383, Short.MAX_VALUE)))) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) + .addComponent(interCasePanel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(intraCasePanel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() + .addGap(21, 21, 21) + .addComponent(dataSourcesLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addContainerGap()))) ); jPanel1Layout.setVerticalGroup( jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel1Layout.createSequentialGroup() .addContainerGap() - .addComponent(commonFilesSearchLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(commonFilesSearchLabel2) + .addComponent(commonItemSearchDescription, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(scopeLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(intraCaseRadio) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(interCaseRadio) - .addGap(2, 2, 2) - .addComponent(layoutPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(categoriesLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(selectedFileCategoriesButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(pictureVideoCheckbox) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(documentsCheckbox) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(allFileCategoriesRadioButton) + .addComponent(interCasePanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(0, 0, 0) + .addComponent(intraCasePanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(percentageThresholdCheck) - .addComponent(percentageThresholdTextOne, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(percentageThresholdInputBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(percentageThresholdTextTwo)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(filler2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(filler1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(searchButton) - .addComponent(cancelButton) - .addComponent(errorText, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addComponent(dataSourcesLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, Short.MAX_VALUE) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(searchButton) + .addComponent(errorText, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addContainerGap()) ); @@ -679,62 +642,51 @@ public final class CommonAttributePanel extends javax.swing.JDialog { private void percentageThresholdCheckActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_percentageThresholdCheckActionPerformed if (this.percentageThresholdCheck.isSelected()) { - this.percentageThresholdTextOne.setEnabled(true); + this.percentageThresholdInputBox.setEnabled(true); } else { - this.percentageThresholdTextOne.setEnabled(false); + this.percentageThresholdInputBox.setEnabled(false); } this.handleFrequencyPercentageState(); }//GEN-LAST:event_percentageThresholdCheckActionPerformed private void interCaseRadioActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_interCaseRadioActionPerformed - ((java.awt.CardLayout) this.layoutPanel.getLayout()).last(this.layoutPanel); + intraCasePanel.setVisible(false); + interCasePanel.setVisible(true); }//GEN-LAST:event_interCaseRadioActionPerformed private void intraCaseRadioActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_intraCaseRadioActionPerformed - ((java.awt.CardLayout) this.layoutPanel.getLayout()).first(this.layoutPanel); + intraCasePanel.setVisible(true); + interCasePanel.setVisible(false); }//GEN-LAST:event_intraCaseRadioActionPerformed - private void documentsCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_documentsCheckboxActionPerformed - this.handleFileTypeCheckBoxState(); - }//GEN-LAST:event_documentsCheckboxActionPerformed - - private void pictureVideoCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_pictureVideoCheckboxActionPerformed - this.handleFileTypeCheckBoxState(); - }//GEN-LAST:event_pictureVideoCheckboxActionPerformed - - private void selectedFileCategoriesButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_selectedFileCategoriesButtonActionPerformed - this.handleFileTypeCheckBoxState(); - }//GEN-LAST:event_selectedFileCategoriesButtonActionPerformed - - private void allFileCategoriesRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_allFileCategoriesRadioButtonActionPerformed - this.handleFileTypeCheckBoxState(); - }//GEN-LAST:event_allFileCategoriesRadioButtonActionPerformed - - private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed - SwingUtilities.windowForComponent(this).dispose(); - }//GEN-LAST:event_cancelButtonActionPerformed - private void searchButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_searchButtonActionPerformed search(); SwingUtilities.windowForComponent(this).dispose(); }//GEN-LAST:event_searchButtonActionPerformed - private void percentageThresholdChanged(){ - String percentageString = this.percentageThresholdTextOne.getText(); + /** + * Convert the text in the percentage threshold input box into an integer, + * -1 is used when the string can not be converted to an integer. + */ + private void percentageThresholdChanged() { + String percentageString = this.percentageThresholdInputBox.getText(); try { this.percentageThresholdValue = Integer.parseInt(percentageString); - - } catch (NumberFormatException exception) { + + } catch (NumberFormatException ignored) { this.percentageThresholdValue = -1; } - this.handleFrequencyPercentageState(); + this.handleFrequencyPercentageState(); } - - private void updateErrorTextAndSearchBox() { + /** + * Update the error text and the enabled status of the search button to + * reflect the current validity of the search settings. + */ + private void updateErrorTextAndSearchButton() { if (this.errorManager.anyErrors()) { this.searchButton.setEnabled(false); //grab the first error error and show it @@ -746,83 +698,90 @@ public final class CommonAttributePanel extends javax.swing.JDialog { } } - private void enablePercentageOptions() { - this.percentageThresholdTextOne.setEnabled(true); - this.percentageThresholdCheck.setEnabled(true); - this.percentageThresholdCheck.setSelected(true); - this.percentageThresholdTextTwo.setEnabled(true); - } - - private void disablePercentageOptions() { - this.percentageThresholdTextOne.setEnabled(false); - this.percentageThresholdCheck.setEnabled(false); - this.percentageThresholdCheck.setSelected(false); - this.percentageThresholdTextTwo.setEnabled(false); - } - - private void handleFileTypeCheckBoxState() { - - this.pictureViewCheckboxState = this.pictureVideoCheckbox.isSelected(); - this.documentsCheckboxState = this.documentsCheckbox.isSelected(); - - if (this.allFileCategoriesRadioButton.isSelected()) { - this.pictureVideoCheckbox.setEnabled(false); - this.documentsCheckbox.setEnabled(false); - - this.errorManager.setError(UserInputErrorManager.NO_FILE_CATEGORIES_SELECTED_KEY, false); - } - - if (this.selectedFileCategoriesButton.isSelected()) { - - this.pictureVideoCheckbox.setSelected(this.pictureViewCheckboxState); - this.documentsCheckbox.setSelected(this.documentsCheckboxState); - - this.pictureVideoCheckbox.setEnabled(true); - this.documentsCheckbox.setEnabled(true); - - if (!this.pictureVideoCheckbox.isSelected() && !this.documentsCheckbox.isSelected() && !this.allFileCategoriesRadioButton.isSelected()) { - this.errorManager.setError(UserInputErrorManager.NO_FILE_CATEGORIES_SELECTED_KEY, true); - } else { - this.errorManager.setError(UserInputErrorManager.NO_FILE_CATEGORIES_SELECTED_KEY, false); - } - } - - this.updateErrorTextAndSearchBox(); + /** + * Update the percentage options to reflect the number of data sources + * available. + * + * @param numberOfDataSources the number of data sources available in the + * central repository + */ + @NbBundle.Messages({ + "# {0} - number of datasources", + "CommonAttributePanel.dataSourcesLabel.text=The current Central Repository contains {0} data source(s)."}) + private void updatePercentageOptions(Long numberOfDataSources) { + boolean enabled = numberOfDataSources > 0L; + String numberOfDataSourcesText = enabled ? Bundle.CommonAttributePanel_dataSourcesLabel_text(numberOfDataSources) : ""; + this.dataSourcesLabel.setText(numberOfDataSourcesText); + this.percentageThresholdInputBox.setEnabled(enabled); + this.percentageThresholdCheck.setEnabled(enabled); + this.percentageThresholdCheck.setSelected(enabled); + this.percentageThresholdTextTwo.setEnabled(enabled); + } + /** + * Check that the integer value of what is entered in the percentage + * threshold text box is a valid percentage and update the errorManager to + * reflect the validity. + */ private void handleFrequencyPercentageState() { if (this.percentageThresholdValue > 0 && this.percentageThresholdValue <= 100) { this.errorManager.setError(UserInputErrorManager.FREQUENCY_PERCENTAGE_OUT_OF_RANGE_KEY, false); } else { + this.errorManager.setError(UserInputErrorManager.FREQUENCY_PERCENTAGE_OUT_OF_RANGE_KEY, true); } - - this.updateErrorTextAndSearchBox(); + this.updateErrorTextAndSearchButton(); } // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JRadioButton allFileCategoriesRadioButton; - private javax.swing.JButton cancelButton; - private javax.swing.JLabel categoriesLabel; - private javax.swing.JLabel commonFilesSearchLabel1; - private javax.swing.JLabel commonFilesSearchLabel2; - private javax.swing.JCheckBox documentsCheckbox; + private javax.swing.JLabel commonItemSearchDescription; + private javax.swing.JLabel dataSourcesLabel; private javax.swing.JLabel errorText; - private javax.swing.ButtonGroup fileTypeFilterButtonGroup; - private javax.swing.Box.Filler filler1; - private javax.swing.Box.Filler filler2; private org.sleuthkit.autopsy.commonfilesearch.InterCasePanel interCasePanel; private javax.swing.JRadioButton interCaseRadio; private javax.swing.ButtonGroup interIntraButtonGroup; private org.sleuthkit.autopsy.commonfilesearch.IntraCasePanel intraCasePanel; private javax.swing.JRadioButton intraCaseRadio; private javax.swing.JPanel jPanel1; - private java.awt.Panel layoutPanel; private javax.swing.JCheckBox percentageThresholdCheck; - private javax.swing.JTextField percentageThresholdTextOne; + private javax.swing.JTextField percentageThresholdInputBox; private javax.swing.JLabel percentageThresholdTextTwo; - private javax.swing.JCheckBox pictureVideoCheckbox; + private javax.swing.JLabel scopeLabel; private javax.swing.JButton searchButton; - private javax.swing.JRadioButton selectedFileCategoriesButton; // End of variables declaration//GEN-END:variables + + /** + * Add this panel as an observer of it's sub panels so that errors can be + * indicated accurately. + */ + void observeSubPanels() { + intraCasePanel.addObserver(this); + interCasePanel.addObserver(this); + } + + /** + * Check that the sub panels have valid options selected regarding their + * file type filtering options, and update the errorManager with the + * validity. + */ + private void checkFileTypeCheckBoxState() { + boolean validCheckBoxState = true; + if (CommonAttributePanel.this.interCaseRadio.isSelected()) { + if (interCasePanel.fileCategoriesButtonIsSelected()) { + validCheckBoxState = interCasePanel.pictureVideoCheckboxIsSelected() || interCasePanel.documentsCheckboxIsSelected(); + } + } else { + if (intraCasePanel.fileCategoriesButtonIsSelected()) { + validCheckBoxState = intraCasePanel.pictureVideoCheckboxIsSelected() || intraCasePanel.documentsCheckboxIsSelected(); + } + } + if (validCheckBoxState) { + this.errorManager.setError(UserInputErrorManager.NO_FILE_CATEGORIES_SELECTED_KEY, false); + } else { + this.errorManager.setError(UserInputErrorManager.NO_FILE_CATEGORIES_SELECTED_KEY, true); + } + this.updateErrorTextAndSearchButton(); + } + } diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonFilesSearchAction.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributeSearchAction.java similarity index 58% rename from Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonFilesSearchAction.java rename to Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributeSearchAction.java index 6cc00256dd..66a3170c4a 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonFilesSearchAction.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributeSearchAction.java @@ -24,72 +24,81 @@ import org.openide.util.HelpCtx; import org.openide.util.NbBundle; import org.openide.util.actions.CallableSystemAction; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; -import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.autopsy.coreutils.Logger; /** * Encapsulates a menu action which triggers the common files search dialog. */ -final public class CommonFilesSearchAction extends CallableSystemAction { +final public class CommonAttributeSearchAction extends CallableSystemAction { - private static final Logger LOGGER = Logger.getLogger(CommonFilesSearchAction.class.getName()); - - private static CommonFilesSearchAction instance = null; + private static final Logger LOGGER = Logger.getLogger(CommonAttributeSearchAction.class.getName()); + + private static CommonAttributeSearchAction instance = null; private static final long serialVersionUID = 1L; - - CommonFilesSearchAction() { + + /** + * Get the default CommonAttributeSearchAction. + * + * @return the default instance of this action + */ + public static synchronized CommonAttributeSearchAction getDefault() { + if (instance == null) { + instance = new CommonAttributeSearchAction(); + } + return instance; + } + + /** + * Create a CommonAttributeSearchAction for opening the common attribute + * search dialog + */ + private CommonAttributeSearchAction() { super(); this.setEnabled(false); } @Override - public boolean isEnabled(){ + public boolean isEnabled() { boolean shouldBeEnabled = false; try { //dont refactor any of this to pull out common expressions - order of evaluation of each expression is significant - shouldBeEnabled = - (Case.isCaseOpen() && - Case.getCurrentCase().getDataSources().size() > 1) - || - (EamDb.isEnabled() && - EamDb.getInstance() != null && - EamDb.getInstance().getCases().size() > 1 && - Case.isCaseOpen() && - Case.getCurrentCase() != null && - EamDb.getInstance().getCase(Case.getCurrentCase()) != null); - - } catch(TskCoreException ex) { + shouldBeEnabled + = (Case.isCaseOpen() + && Case.getCurrentCase().getDataSources().size() > 1) + || CommonAttributePanel.isEamDbAvailableForIntercaseSearch(); + + } catch (TskCoreException ex) { LOGGER.log(Level.SEVERE, "Error getting data sources for action enabled check", ex); - } catch (EamDbException ex) { - LOGGER.log(Level.SEVERE, "Error getting CR cases for action enabled check", ex); } return super.isEnabled() && shouldBeEnabled; } - public static synchronized CommonFilesSearchAction getDefault() { - if (instance == null) { - instance = new CommonFilesSearchAction(); - } - return instance; - } - @Override public void actionPerformed(ActionEvent event) { - new CommonAttributePanel().setVisible(true); + createAndShowPanel(); } @Override public void performAction() { - new CommonAttributePanel().setVisible(true); + createAndShowPanel(); + } + + /** + * Create the commonAttributePanel and diplay it. + */ + private void createAndShowPanel() { + CommonAttributePanel commonAttributePanel = new CommonAttributePanel(); + //In order to update errors the CommonAttributePanel needs to observe its sub panels + commonAttributePanel.observeSubPanels(); + commonAttributePanel.setVisible(true); } @NbBundle.Messages({ - "CommonFilesAction.getName.text=Common Files Search"}) + "CommonAttributeSearchAction.getName.text=Common Property Search"}) @Override public String getName() { - return Bundle.CommonFilesAction_getName_text(); + return Bundle.CommonAttributeSearchAction_getName_text(); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributeSearchResults.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributeSearchResults.java index bac59fde12..d7761b47c3 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributeSearchResults.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributeSearchResults.java @@ -25,9 +25,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.logging.Level; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; +import org.sleuthkit.autopsy.coreutils.Logger; /** * Stores the results from the various types of common attribute searching @@ -35,10 +38,13 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; */ final public class CommonAttributeSearchResults { + private static final Logger LOGGER = Logger.getLogger(CommonAttributeSearchResults.class.getName()); + // maps instance count to list of attribute values. private final Map instanceCountToAttributeValues; private final int percentageThreshold; + private final int resultTypeId; /** * Create a values object which can be handed off to the node factories. @@ -46,10 +52,24 @@ final public class CommonAttributeSearchResults { * @param values list of CommonAttributeValue indexed by size of * CommonAttributeValue */ + CommonAttributeSearchResults(Map metadata, int percentageThreshold, CorrelationAttributeInstance.Type resultType) { + //wrap in a new object in case any client code has used an unmodifiable collection + this.instanceCountToAttributeValues = new HashMap<>(metadata); + this.percentageThreshold = percentageThreshold; + this.resultTypeId = resultType.getId(); + } + + /** + * Create a values object which can be handed off to the node factories. + * + * @param values list of CommonAttributeValue indexed by size of + * CommonAttributeValue + */ CommonAttributeSearchResults(Map metadata, int percentageThreshold) { //wrap in a new object in case any client code has used an unmodifiable collection this.instanceCountToAttributeValues = new HashMap<>(metadata); this.percentageThreshold = percentageThreshold; + this.resultTypeId = CorrelationAttributeInstance.FILES_TYPE_ID; } /** @@ -86,7 +106,7 @@ final public class CommonAttributeSearchResults { * search. * * Remove results which are not found in the portion of available data - sources described by maximumPercentageThreshold. + * sources described by maximumPercentageThreshold. * * @return metadata */ @@ -96,15 +116,18 @@ final public class CommonAttributeSearchResults { return Collections.unmodifiableMap(this.instanceCountToAttributeValues); } - CorrelationAttributeInstance.Type fileAttributeType = CorrelationAttributeInstance + CorrelationAttributeInstance.Type attributeType = CorrelationAttributeInstance .getDefaultCorrelationTypes() .stream() - .filter(filterType -> filterType.getId() == CorrelationAttributeInstance.FILES_TYPE_ID) + .filter(filterType -> filterType.getId() == this.resultTypeId) .findFirst().get(); EamDb eamDb = EamDb.getInstance(); Map> itemsToRemove = new HashMap<>(); + //Call countUniqueDataSources once to reduce the number of DB queries needed to get + //the frequencyPercentage + Double uniqueCaseDataSourceTuples = eamDb.getCountUniqueDataSources().doubleValue(); for(Entry listOfValues : Collections.unmodifiableMap(this.instanceCountToAttributeValues).entrySet()){ @@ -113,16 +136,23 @@ final public class CommonAttributeSearchResults { for(CommonAttributeValue value : values.getDelayedMetadataList()){ // Need the real metadata - int frequencyPercentage = eamDb.getFrequencyPercentage(new CorrelationAttributeInstance(fileAttributeType, value.getValue())); + try { + Double uniqueTypeValueTuples = eamDb.getCountUniqueCaseDataSourceTuplesHavingTypeValue( + attributeType, value.getValue()).doubleValue(); + Double commonalityPercentage = uniqueTypeValueTuples / uniqueCaseDataSourceTuples * 100; + int frequencyPercentage = commonalityPercentage.intValue(); - if(frequencyPercentage > maximumPercentageThreshold){ - if(itemsToRemove.containsKey(key)){ - itemsToRemove.get(key).add(value); - } else { - List toRemove = new ArrayList<>(); - toRemove.add(value); - itemsToRemove.put(key, toRemove); + if(frequencyPercentage > maximumPercentageThreshold){ + if(itemsToRemove.containsKey(key)){ + itemsToRemove.get(key).add(value); + } else { + List toRemove = new ArrayList<>(); + toRemove.add(value); + itemsToRemove.put(key, toRemove); + } } + } catch(CorrelationAttributeNormalizationException ex){ + LOGGER.log(Level.WARNING, "Unable to determine frequency percentage attribute - frequency filter may not be accurate for these results.", ex); } } } @@ -154,10 +184,10 @@ final public class CommonAttributeSearchResults { int count = 0; for (CommonAttributeValueList data : this.instanceCountToAttributeValues.values()) { - for(CommonAttributeValue md5 : data.getMetadataList()){ + for(CommonAttributeValue md5 : data.getDelayedMetadataList()){ count += md5.getInstanceCount(); } } return count; } -} +} \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributeValue.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributeValue.java index f6372c89f7..967a205a73 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributeValue.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributeValue.java @@ -86,7 +86,4 @@ final public class CommonAttributeValue { public int getInstanceCount() { return this.fileInstances.size(); } - - - } diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributeValueNode.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributeValueNode.java index c7c5dea58b..219072fa2a 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributeValueNode.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/CommonAttributeValueNode.java @@ -41,7 +41,7 @@ public class CommonAttributeValueNode extends DisplayableItemNode { private final String dataSources; @NbBundle.Messages({ - "Md5Node.Md5Node.format=MD5: %s" + "CommonAttributeValueNode.CommonAttributeValueNode.format=Value: %s" }) /** * Create a Match node whose children will all have this object in common. @@ -57,7 +57,7 @@ public class CommonAttributeValueNode extends DisplayableItemNode { this.dataSources = String.join(", ", data.getDataSources()); this.value = data.getValue(); - this.setDisplayName(String.format(Bundle.Md5Node_Md5Node_format(), this.value)); + this.setDisplayName(String.format(Bundle.CommonAttributeValueNode_CommonAttributeValueNode_format(), this.value)); this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/fileset-icon-16.png"); //NON-NLS } diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/InterCaseCommonAttributeSearcher.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/InterCaseCommonAttributeSearcher.java index d1bff54c99..931ee7ffcc 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/InterCaseCommonAttributeSearcher.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/InterCaseCommonAttributeSearcher.java @@ -23,6 +23,7 @@ import java.util.Map; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationCase; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance.Type; /** * Provides logic for selecting common files from all data sources and all cases @@ -31,6 +32,10 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; abstract class InterCaseCommonAttributeSearcher extends AbstractCommonAttributeSearcher { private final EamDb dbManager; + /** + * The Correlation Type to find matches on. + */ + final Type corAttrType; /** * Implements the algorithm for getting common files across all data sources @@ -43,9 +48,10 @@ abstract class InterCaseCommonAttributeSearcher extends AbstractCommonAttributeS * * @throws EamDbException */ - InterCaseCommonAttributeSearcher(Map dataSourceIdMap, boolean filterByMediaMimeType, boolean filterByDocMimeType, int percentageThreshold) throws EamDbException { + InterCaseCommonAttributeSearcher(Map dataSourceIdMap, boolean filterByMediaMimeType, boolean filterByDocMimeType, Type corAttrType, int percentageThreshold) throws EamDbException { super(dataSourceIdMap, filterByMediaMimeType, filterByDocMimeType, percentageThreshold); dbManager = EamDb.getInstance(); + this.corAttrType = corAttrType; } protected CorrelationCase getCorrelationCaseFromId(int correlationCaseId) throws EamDbException { @@ -56,4 +62,5 @@ abstract class InterCaseCommonAttributeSearcher extends AbstractCommonAttributeS } throw new IllegalArgumentException("Cannot locate case."); } + } diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/InterCasePanel.form b/Core/src/org/sleuthkit/autopsy/commonfilesearch/InterCasePanel.form index 8c62356803..167e9acd48 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/InterCasePanel.form +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/InterCasePanel.form @@ -20,60 +20,72 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -85,5 +97,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/InterCasePanel.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/InterCasePanel.java index 8fc4efd01e..70c38af896 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/InterCasePanel.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/InterCasePanel.java @@ -1,16 +1,16 @@ /* - * + * * Autopsy Forensic Browser - * + * * Copyright 2018 Basis Technology Corp. * Contact: carrier sleuthkit org - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -21,45 +21,110 @@ package org.sleuthkit.autopsy.commonfilesearch; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Observable; +import java.util.Observer; import javax.swing.ComboBoxModel; +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; /** * UI controls for Common Files Search scenario where the user intends to find * common files between cases in addition to the present case. */ -public class InterCasePanel extends javax.swing.JPanel { - +public final class InterCasePanel extends javax.swing.JPanel { + private static final long serialVersionUID = 1L; - + private final Observable fileTypeFilterObservable; static final int NO_CASE_SELECTED = -1; - + private ComboBoxModel casesList = new DataSourceComboBoxModel(); - + private final Map caseMap; - - //True if we are looking in any or all cases, - // false if we must find matches in a given case plus the current case - private boolean anyCase; - + + private Map correlationTypeFilters; + /** * Creates new form InterCasePanel */ public InterCasePanel() { initComponents(); this.caseMap = new HashMap<>(); - this.anyCase = true; + fileTypeFilterObservable = new Observable() { + @Override + public void notifyObservers() { + //set changed before notify observers + //we want this observerable to always cause the observer to update when notified + this.setChanged(); + super.notifyObservers(); + } + }; } - private void specificCaseSelected(boolean selected) { - this.specificCentralRepoCaseRadio.setEnabled(selected); - if (this.specificCentralRepoCaseRadio.isEnabled()) { - this.caseComboBox.setEnabled(true); - this.caseComboBox.setSelectedIndex(0); - } + /** + * Add an Observer to the Observable portion of this panel so that it can be + * notified of changes to this panel. + * + * @param observer the object which is observing this panel + */ + void addObserver(Observer observer) { + fileTypeFilterObservable.addObserver(observer); } - + + /** + * If the user has selected to show only results of specific file types. + * + * @return if the selected file categories button is enabled AND selected, + * true for enabled AND selected false for not selected OR not + * enabled + */ + boolean fileCategoriesButtonIsSelected() { + return selectedFileCategoriesButton.isEnabled() && selectedFileCategoriesButton.isSelected(); + } + + /** + * If the user has selected selected to show Picture and Video files as part + * of the filtered results. + * + * @return if the pictures and video checkbox is enabled AND selected, true + * for enabled AND selected false for not selected OR not enabled + */ + boolean pictureVideoCheckboxIsSelected() { + return pictureVideoCheckbox.isEnabled() && pictureVideoCheckbox.isSelected(); + } + + /** + * If the user has selected selected to show Document files as part of the + * filtered results. + * + * @return if the documents checkbox is enabled AND selected, true for + * enabled AND selected false for not selected OR not enabled + */ + boolean documentsCheckboxIsSelected() { + return documentsCheckbox.isEnabled() && documentsCheckbox.isSelected(); + } + + /** + * If the EamDB is enabled, the UI will populate the correlation type + * ComboBox with available types in the CR. + */ + void setupCorrelationTypeFilter() { + this.correlationTypeFilters = new HashMap<>(); + try { + List types = CorrelationAttributeInstance.getDefaultCorrelationTypes(); + for (CorrelationAttributeInstance.Type type : types) { + correlationTypeFilters.put(type.getDisplayName(), type); + this.correlationTypeComboBox.addItem(type.getDisplayName()); + } + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + } + this.correlationTypeComboBox.setSelectedIndex(0); + } + /** * 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 @@ -70,115 +135,261 @@ public class InterCasePanel extends javax.swing.JPanel { private void initComponents() { buttonGroup = new javax.swing.ButtonGroup(); - anyCentralRepoCaseRadio = new javax.swing.JRadioButton(); - specificCentralRepoCaseRadio = new javax.swing.JRadioButton(); caseComboBox = new javax.swing.JComboBox<>(); - - buttonGroup.add(anyCentralRepoCaseRadio); - anyCentralRepoCaseRadio.setSelected(true); - org.openide.awt.Mnemonics.setLocalizedText(anyCentralRepoCaseRadio, org.openide.util.NbBundle.getMessage(InterCasePanel.class, "InterCasePanel.anyCentralRepoCaseRadio.text")); // NOI18N - anyCentralRepoCaseRadio.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - anyCentralRepoCaseRadioActionPerformed(evt); - } - }); - - buttonGroup.add(specificCentralRepoCaseRadio); - org.openide.awt.Mnemonics.setLocalizedText(specificCentralRepoCaseRadio, org.openide.util.NbBundle.getMessage(InterCasePanel.class, "InterCasePanel.specificCentralRepoCaseRadio.text")); // NOI18N - specificCentralRepoCaseRadio.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - specificCentralRepoCaseRadioActionPerformed(evt); - } - }); + correlationComboBoxLabel = new javax.swing.JLabel(); + correlationTypeComboBox = new javax.swing.JComboBox<>(); + categoriesLabel = new javax.swing.JLabel(); + allFileCategoriesRadioButton = new javax.swing.JRadioButton(); + selectedFileCategoriesButton = new javax.swing.JRadioButton(); + pictureVideoCheckbox = new javax.swing.JCheckBox(); + documentsCheckbox = new javax.swing.JCheckBox(); + specificCentralRepoCaseCheckbox = new javax.swing.JCheckBox(); caseComboBox.setModel(casesList); caseComboBox.setEnabled(false); + org.openide.awt.Mnemonics.setLocalizedText(correlationComboBoxLabel, org.openide.util.NbBundle.getMessage(InterCasePanel.class, "InterCasePanel.correlationComboBoxLabel.text")); // NOI18N + + correlationTypeComboBox.setSelectedItem(null); + correlationTypeComboBox.setToolTipText(org.openide.util.NbBundle.getMessage(InterCasePanel.class, "InterCasePanel.correlationTypeComboBox.toolTipText")); // NOI18N + correlationTypeComboBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + correlationTypeComboBoxActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(categoriesLabel, org.openide.util.NbBundle.getMessage(InterCasePanel.class, "InterCasePanel.categoriesLabel.text")); // NOI18N + categoriesLabel.setEnabled(false); + categoriesLabel.setName(""); // NOI18N + + buttonGroup.add(allFileCategoriesRadioButton); + allFileCategoriesRadioButton.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(allFileCategoriesRadioButton, org.openide.util.NbBundle.getMessage(InterCasePanel.class, "InterCasePanel.allFileCategoriesRadioButton.text")); // NOI18N + allFileCategoriesRadioButton.setToolTipText(org.openide.util.NbBundle.getMessage(InterCasePanel.class, "InterCasePanel.allFileCategoriesRadioButton.toolTipText")); // NOI18N + allFileCategoriesRadioButton.setEnabled(false); + allFileCategoriesRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + allFileCategoriesRadioButtonActionPerformed(evt); + } + }); + + buttonGroup.add(selectedFileCategoriesButton); + org.openide.awt.Mnemonics.setLocalizedText(selectedFileCategoriesButton, org.openide.util.NbBundle.getMessage(InterCasePanel.class, "InterCasePanel.selectedFileCategoriesButton.text")); // NOI18N + selectedFileCategoriesButton.setToolTipText(org.openide.util.NbBundle.getMessage(InterCasePanel.class, "InterCasePanel.selectedFileCategoriesButton.toolTipText")); // NOI18N + selectedFileCategoriesButton.setEnabled(false); + selectedFileCategoriesButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + selectedFileCategoriesButtonActionPerformed(evt); + } + }); + + pictureVideoCheckbox.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(pictureVideoCheckbox, org.openide.util.NbBundle.getMessage(InterCasePanel.class, "InterCasePanel.pictureVideoCheckbox.text")); // NOI18N + pictureVideoCheckbox.setEnabled(false); + pictureVideoCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + pictureVideoCheckboxActionPerformed(evt); + } + }); + + documentsCheckbox.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(documentsCheckbox, org.openide.util.NbBundle.getMessage(InterCasePanel.class, "InterCasePanel.documentsCheckbox.text")); // NOI18N + documentsCheckbox.setEnabled(false); + documentsCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + documentsCheckboxActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(specificCentralRepoCaseCheckbox, org.openide.util.NbBundle.getMessage(InterCasePanel.class, "InterCasePanel.specificCentralRepoCaseCheckbox.text")); // NOI18N + specificCentralRepoCaseCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + specificCentralRepoCaseCheckboxActionPerformed(evt); + } + }); + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(specificCentralRepoCaseCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(anyCentralRepoCaseRadio) + .addComponent(correlationComboBoxLabel) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(categoriesLabel) + .addGroup(layout.createSequentialGroup() + .addGap(19, 19, 19) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(allFileCategoriesRadioButton) + .addComponent(selectedFileCategoriesButton) + .addGroup(layout.createSequentialGroup() + .addGap(21, 21, 21) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(documentsCheckbox) + .addComponent(pictureVideoCheckbox)))))) + .addGap(0, 0, Short.MAX_VALUE)) .addGroup(layout.createSequentialGroup() .addGap(21, 21, 21) - .addComponent(caseComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 261, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addComponent(specificCentralRepoCaseRadio)) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(caseComboBox, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(correlationTypeComboBox, 0, 353, Short.MAX_VALUE)))) + .addContainerGap()) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() - .addComponent(anyCentralRepoCaseRadio) + .addContainerGap() + .addComponent(specificCentralRepoCaseCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, 18, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(specificCentralRepoCaseRadio) + .addComponent(caseComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(caseComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(correlationComboBoxLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(correlationTypeComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(categoriesLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(allFileCategoriesRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(selectedFileCategoriesButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(pictureVideoCheckbox) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(documentsCheckbox) + .addGap(0, 0, 0)) ); }// //GEN-END:initComponents - private void specificCentralRepoCaseRadioActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_specificCentralRepoCaseRadioActionPerformed - this.caseComboBox.setEnabled(true); - if(this.caseComboBox.isEnabled() && this.caseComboBox.getSelectedItem() == null){ + private void allFileCategoriesRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_allFileCategoriesRadioButtonActionPerformed + //When the allFileCategoriesRadioButton is selected disable the options + //related to selected file categories and notify observers that the panel has changed + //incase the current settings are invalid + pictureVideoCheckbox.setEnabled(false); + documentsCheckbox.setEnabled(false); + fileTypeFilterObservable.notifyObservers(); + }//GEN-LAST:event_allFileCategoriesRadioButtonActionPerformed + + private void selectedFileCategoriesButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_selectedFileCategoriesButtonActionPerformed + //When the selectedFileCategoriesButton is selected enable its related options + //and notify observers that the panel has changed incase the current settings are invalid + pictureVideoCheckbox.setEnabled(true); + documentsCheckbox.setEnabled(true); + fileTypeFilterObservable.notifyObservers(); + }//GEN-LAST:event_selectedFileCategoriesButtonActionPerformed + + private void specificCentralRepoCaseCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_specificCentralRepoCaseCheckboxActionPerformed + //When the specificCentralRepoCaseCheckbox is clicked update its related options + this.caseComboBox.setEnabled(specificCentralRepoCaseCheckbox.isSelected()); + if (specificCentralRepoCaseCheckbox.isSelected()) { this.caseComboBox.setSelectedIndex(0); } - this.anyCase = false; - }//GEN-LAST:event_specificCentralRepoCaseRadioActionPerformed + }//GEN-LAST:event_specificCentralRepoCaseCheckboxActionPerformed - private void anyCentralRepoCaseRadioActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_anyCentralRepoCaseRadioActionPerformed - this.caseComboBox.setEnabled(false); - this.anyCase = true; - }//GEN-LAST:event_anyCentralRepoCaseRadioActionPerformed + + private void correlationTypeComboBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_correlationTypeComboBoxActionPerformed + //we do not currenlty have mime type in the central repository so this section will always be disabled + boolean enableFileTypesFilter = false; //this.correlationTypeComboBox.getSelectedItem().equals("Files"); + categoriesLabel.setEnabled(enableFileTypesFilter); + allFileCategoriesRadioButton.setEnabled(enableFileTypesFilter); + selectedFileCategoriesButton.setEnabled(enableFileTypesFilter); + boolean enableIndividualFilters = (enableFileTypesFilter && selectedFileCategoriesButton.isSelected()); + pictureVideoCheckbox.setEnabled(enableIndividualFilters); + documentsCheckbox.setEnabled(enableIndividualFilters); + }//GEN-LAST:event_correlationTypeComboBoxActionPerformed + + private void pictureVideoCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_pictureVideoCheckboxActionPerformed + //notify observers that the panel has changed incase the current settings are invalid + fileTypeFilterObservable.notifyObservers(); + }//GEN-LAST:event_pictureVideoCheckboxActionPerformed + + private void documentsCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_documentsCheckboxActionPerformed + //notify observers that the panel has changed incase the current settings are invalid + fileTypeFilterObservable.notifyObservers(); + }//GEN-LAST:event_documentsCheckboxActionPerformed // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JRadioButton anyCentralRepoCaseRadio; + private javax.swing.JRadioButton allFileCategoriesRadioButton; private javax.swing.ButtonGroup buttonGroup; private javax.swing.JComboBox caseComboBox; - private javax.swing.JRadioButton specificCentralRepoCaseRadio; + private javax.swing.JLabel categoriesLabel; + private javax.swing.JLabel correlationComboBoxLabel; + private javax.swing.JComboBox correlationTypeComboBox; + private javax.swing.JCheckBox documentsCheckbox; + private javax.swing.JCheckBox pictureVideoCheckbox; + private javax.swing.JRadioButton selectedFileCategoriesButton; + private javax.swing.JCheckBox specificCentralRepoCaseCheckbox; // End of variables declaration//GEN-END:variables + /** + * Get the map of cases which was used to populate the combo box on + * this panel. + * + * @return an unmodifiable copy of the map of cases + */ Map getCaseMap() { return Collections.unmodifiableMap(this.caseMap); } - void setCaseList(DataSourceComboBoxModel dataSourceComboBoxModel) { + /** + * Set the datamodel for the combo box which displays the cases in + * the central repository + * + * @param dataSourceComboBoxModel the DataSourceComboBoxModel to use + */ + void setCaseComboboxModel(DataSourceComboBoxModel dataSourceComboBoxModel) { this.casesList = dataSourceComboBoxModel; this.caseComboBox.setModel(dataSourceComboBoxModel); } - void rigForMultipleCases(boolean multipleCases) { - this.anyCentralRepoCaseRadio.setEnabled(multipleCases); - this.anyCentralRepoCaseRadio.setEnabled(multipleCases); - - if(!multipleCases){ - this.specificCentralRepoCaseRadio.setSelected(true); - this.specificCaseSelected(true); - } - } - + /** + * Update the map of cases that this panel allows the user to select from + * + * @param caseMap A map of cases + */ void setCaseMap(Map caseMap) { this.caseMap.clear(); this.caseMap.putAll(caseMap); } + /** + * Whether or not the central repository has multiple cases in it. + * + * @return true when the central repository has 2 or more case, false if it + * has 0 or 1 case in it. + */ boolean centralRepoHasMultipleCases() { return this.caseMap.size() >= 2; } - - Integer getSelectedCaseId(){ - if(this.anyCase){ - return InterCasePanel.NO_CASE_SELECTED; - } - - for(Entry entry : this.caseMap.entrySet()){ - if(entry.getValue().equals(this.caseComboBox.getSelectedItem())){ - return entry.getKey(); + + /** + * Get the ID for the selected case + * + * @return the ID of the selected case or InterCasePanel.NO_CASE_SELECTED + * when none selected + */ + Integer getSelectedCaseId() { + if (specificCentralRepoCaseCheckbox.isSelected()) { + for (Entry entry : this.caseMap.entrySet()) { + if (entry.getValue().equals(this.caseComboBox.getSelectedItem())) { + return entry.getKey(); + } } } - return InterCasePanel.NO_CASE_SELECTED; } + + /** + * Returns the selected Correlation Type by getting the Type from the stored + * HashMap. + * + * @return Type the selected Correlation Type to query for. + */ + CorrelationAttributeInstance.Type getSelectedCorrelationType() { + return correlationTypeFilters.get(this.correlationTypeComboBox.getSelectedItem().toString()); + } } diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/InterCaseSearchResultsProcessor.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/InterCaseSearchResultsProcessor.java index 16ee2bd23a..d258db2cc1 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/InterCaseSearchResultsProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/InterCaseSearchResultsProcessor.java @@ -26,10 +26,13 @@ import java.util.Map; import java.util.logging.Level; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance.Type; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationCase; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationDataSource; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbUtil; import org.sleuthkit.autopsy.centralrepository.datamodel.InstanceTableCallback; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.TskData; @@ -42,46 +45,87 @@ import org.sleuthkit.datamodel.HashUtility; final class InterCaseSearchResultsProcessor { private Map dataSources; - + + /** + * The CorrelationAttributeInstance.Type this Processor will query on + */ + private final Type correlationType; + private static final Logger LOGGER = Logger.getLogger(CommonAttributePanel.class.getName()); - private final String interCaseWhereClause = "value IN (SELECT value FROM file_instances" - + " WHERE value IN (SELECT value FROM file_instances" - + " WHERE case_id=%s AND (known_status !=%s OR known_status IS NULL) GROUP BY value)" - + " GROUP BY value HAVING COUNT(DISTINCT case_id) > 1) ORDER BY value"; - - private final String singleInterCaseWhereClause = "value IN (SELECT value FROM file_instances " - + "WHERE value IN (SELECT value FROM file_instances " - + "WHERE case_id=%s AND (known_status !=%s OR known_status IS NULL) GROUP BY value) " - + "AND (case_id=%s OR case_id=%s) GROUP BY value HAVING COUNT(DISTINCT case_id) > 1) ORDER BY value"; - /** - * Used in the InterCaseCommonAttributeSearchers to find common attribute instances and generate nodes at the UI level. - * @param dataSources + * The initial CorrelationAttributeInstance ids lookup query. */ - InterCaseSearchResultsProcessor(Map dataSources){ + private final String interCaseWhereClause; + + /** + * The single CorrelationAttributeInstance object retrieval query + */ + private final String singleInterCaseWhereClause; + + /** + * Used in the InterCaseCommonAttributeSearchers to find common attribute + * instances and generate nodes at the UI level. + * + * @param dataSources the cases to filter and correlate on + * @param theType the type of CR data to search + */ + InterCaseSearchResultsProcessor(Map dataSources, CorrelationAttributeInstance.Type theType) { + this.correlationType = theType; this.dataSources = dataSources; + interCaseWhereClause = getInterCaseWhereClause(); + singleInterCaseWhereClause = getSingleInterCaseWhereClause(); } - + + private String getInterCaseWhereClause() { + String tableName = EamDbUtil.correlationTypeToInstanceTableName(correlationType); + StringBuilder sqlString = new StringBuilder(250); + sqlString.append("value IN (SELECT value FROM ") + .append(tableName) + .append(" WHERE value IN (SELECT value FROM ") + .append(tableName) + .append(" WHERE case_id=%s AND (known_status !=%s OR known_status IS NULL) GROUP BY value)") + .append(" GROUP BY value HAVING COUNT(DISTINCT case_id) > 1) ORDER BY value"); + return sqlString.toString(); + } + + private String getSingleInterCaseWhereClause() { + String tableName = EamDbUtil.correlationTypeToInstanceTableName(correlationType); + StringBuilder sqlString = new StringBuilder(250); + sqlString.append("value IN (SELECT value FROM ") + .append(tableName) + .append(" WHERE value IN (SELECT value FROM ") + .append(tableName) + .append(" WHERE case_id=%s AND (known_status !=%s OR known_status IS NULL) GROUP BY value)") + .append(" AND (case_id=%s OR case_id=%s) GROUP BY value HAVING COUNT(DISTINCT case_id) > 1) ORDER BY value"); + return sqlString.toString(); + } + /** - * Used in the CentralRepoCommonAttributeInstance to find common attribute instances and generate nodes at the UI level. + * Used in the CentralRepoCommonAttributeInstance to find common attribute + * instances and generate nodes at the UI level. + * + * @param theType the type of CR data to search */ - InterCaseSearchResultsProcessor(){ - //intentionally emtpy - we need a constructor which does not set the data sources field + InterCaseSearchResultsProcessor(CorrelationAttributeInstance.Type theType) { + this.correlationType = theType; + interCaseWhereClause = getInterCaseWhereClause(); + singleInterCaseWhereClause = getSingleInterCaseWhereClause(); } - + /** * Finds a single CorrelationAttribute given an id. * * @param attrbuteId Row of CorrelationAttribute to retrieve from the EamDb + * * @return CorrelationAttribute object representation of retrieved match */ CorrelationAttributeInstance findSingleCorrelationAttribute(int attrbuteId) { try { + InterCaseCommonAttributeRowCallback instancetableCallback = new InterCaseCommonAttributeRowCallback(); EamDb DbManager = EamDb.getInstance(); - CorrelationAttributeInstance.Type fileType = DbManager.getCorrelationTypeById(CorrelationAttributeInstance.FILES_TYPE_ID); - DbManager.processInstanceTableWhere(fileType, String.format("id = %s", attrbuteId), instancetableCallback); + DbManager.processInstanceTableWhere(correlationType, String.format("id = %s", attrbuteId), instancetableCallback); return instancetableCallback.getCorrelationAttribute(); @@ -102,15 +146,15 @@ final class InterCaseSearchResultsProcessor { try { InterCaseCommonAttributesCallback instancetableCallback = new InterCaseCommonAttributesCallback(); EamDb DbManager = EamDb.getInstance(); - CorrelationAttributeInstance.Type fileType = DbManager.getCorrelationTypeById(CorrelationAttributeInstance.FILES_TYPE_ID); + int caseId = DbManager.getCase(currentCase).getID(); - - DbManager.processInstanceTableWhere(fileType, String.format(interCaseWhereClause, caseId, + + DbManager.processInstanceTableWhere(correlationType, String.format(interCaseWhereClause, caseId, TskData.FileKnown.KNOWN.getFileKnownValue()), instancetableCallback); - + return instancetableCallback.getInstanceCollatedCommonFiles(); - + } catch (EamDbException ex) { LOGGER.log(Level.SEVERE, "Error accessing EamDb processing CaseInstancesTable.", ex); } @@ -123,16 +167,15 @@ final class InterCaseSearchResultsProcessor { * md5 and case. * * @param currentCase The current TSK Case. - * @param singleCase The case of interest. Matches must exist in this case. + * @param singleCase The case of interest. Matches must exist in this case. */ Map findSingleInterCaseCommonAttributeValues(Case currentCase, CorrelationCase singleCase) { try { InterCaseCommonAttributesCallback instancetableCallback = new InterCaseCommonAttributesCallback(); EamDb DbManager = EamDb.getInstance(); - CorrelationAttributeInstance.Type fileType = DbManager.getCorrelationTypeById(CorrelationAttributeInstance.FILES_TYPE_ID); int caseId = DbManager.getCase(currentCase).getID(); int targetCaseId = singleCase.getID(); - DbManager.processInstanceTableWhere(fileType, String.format(singleInterCaseWhereClause, caseId, + DbManager.processInstanceTableWhere(correlationType, String.format(singleInterCaseWhereClause, caseId, TskData.FileKnown.KNOWN.getFileKnownValue(), caseId, targetCaseId), instancetableCallback); return instancetableCallback.getInstanceCollatedCommonFiles(); } catch (EamDbException ex) { @@ -158,27 +201,42 @@ final class InterCaseSearchResultsProcessor { while (resultSet.next()) { int resultId = InstanceTableCallback.getId(resultSet); - String md5Value = InstanceTableCallback.getValue(resultSet); + String corValue = InstanceTableCallback.getValue(resultSet); if (previousRowMd5.isEmpty()) { - previousRowMd5 = md5Value; + previousRowMd5 = corValue; } - if (md5Value == null || HashUtility.isNoDataMd5(md5Value)) { + if (corValue == null || HashUtility.isNoDataMd5(corValue)) { continue; } - countAndAddCommonAttributes(md5Value, resultId); + countAndAddCommonAttributes(corValue, resultId); } + //Add the final instances + CommonAttributeValueList value = new CommonAttributeValueList(); + if (commonAttributeValue != null) { + value.addMetadataToList(commonAttributeValue); + instanceCollatedCommonFiles.put(commonAttributeValue.getInstanceCount(), value); + } } catch (SQLException ex) { LOGGER.log(Level.WARNING, "Error getting artifact instances from database.", ex); // NON-NLS } } - private void countAndAddCommonAttributes(String md5Value, int resultId) { + /** + * Add a resultId to the list of matches for a given corValue, which + * counts to number of instances of that match, determining which + * InstanceCountNode the match will be added to. + * + * @param corValue the value which matches + * @param resultId the CorrelationAttributeInstance id to be retrieved + * later. + */ + private void countAndAddCommonAttributes(String corValue, int resultId) { if (commonAttributeValue == null) { - commonAttributeValue = new CommonAttributeValue(md5Value); + commonAttributeValue = new CommonAttributeValue(corValue); } - if (!md5Value.equals(previousRowMd5)) { + if (!corValue.equals(previousRowMd5)) { int size = commonAttributeValue.getInstanceCount(); if (instanceCollatedCommonFiles.containsKey(size)) { instanceCollatedCommonFiles.get(size).addMetadataToList(commonAttributeValue); @@ -188,13 +246,13 @@ final class InterCaseSearchResultsProcessor { instanceCollatedCommonFiles.put(size, value); } - commonAttributeValue = new CommonAttributeValue(md5Value); - previousRowMd5 = md5Value; + commonAttributeValue = new CommonAttributeValue(corValue); + previousRowMd5 = corValue; } // we don't *have* all the information for the rows in the CR, // so we need to consult the present case via the SleuthkitCase object // Later, when the FileInstanceNode is built. Therefore, build node generators for now. - AbstractCommonAttributeInstance searchResult = new CentralRepoCommonAttributeInstance(resultId, InterCaseSearchResultsProcessor.this.dataSources); + AbstractCommonAttributeInstance searchResult = new CentralRepoCommonAttributeInstance(resultId, InterCaseSearchResultsProcessor.this.dataSources, correlationType); commonAttributeValue.addInstance(searchResult); } @@ -215,16 +273,19 @@ final class InterCaseSearchResultsProcessor { public void process(ResultSet resultSet) { try { EamDb DbManager = EamDb.getInstance(); - CorrelationAttributeInstance.Type fileType = DbManager.getCorrelationTypeById(CorrelationAttributeInstance.FILES_TYPE_ID); while (resultSet.next()) { CorrelationCase correlationCase = DbManager.getCaseById(InstanceTableCallback.getCaseId(resultSet)); CorrelationDataSource dataSource = DbManager.getDataSourceById(correlationCase, InstanceTableCallback.getDataSourceId(resultSet)); - correlationAttributeInstance = DbManager.getCorrelationAttributeInstance(fileType, - correlationCase, - dataSource, - InstanceTableCallback.getValue(resultSet), - InstanceTableCallback.getFilePath(resultSet)); + try { + correlationAttributeInstance = DbManager.getCorrelationAttributeInstance(correlationType, + correlationCase, + dataSource, + InstanceTableCallback.getValue(resultSet), + InstanceTableCallback.getFilePath(resultSet)); + } catch (CorrelationAttributeNormalizationException ex) { + LOGGER.log(Level.INFO, "Unable to get CorrelationAttributeInstance.", ex); // NON-NLS + } } } catch (SQLException | EamDbException ex) { diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/IntraCaseCommonAttributeSearcher.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/IntraCaseCommonAttributeSearcher.java index f02f26b869..569ae5232f 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/IntraCaseCommonAttributeSearcher.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/IntraCaseCommonAttributeSearcher.java @@ -36,8 +36,8 @@ import org.sleuthkit.datamodel.TskCoreException; /** * * Generates a List when - * findFiles() is called, which organizes files by md5 to - * prepare to display in viewer. + * findMatches() is called, which organizes files by md5 to prepare + * to display in viewer. * * This entire thing runs on a background thread where exceptions are handled. */ @@ -93,14 +93,14 @@ public abstract class IntraCaseCommonAttributeSearcher extends AbstractCommonAtt * @throws SQLException */ @Override - public CommonAttributeSearchResults findFiles() throws TskCoreException, NoCurrentCaseException, SQLException { + public CommonAttributeSearchResults findMatches() throws TskCoreException, NoCurrentCaseException, SQLException { Map commonFiles = new HashMap<>(); - + final Case currentCase = Case.getCurrentCaseThrows(); final String caseName = currentCase.getDisplayName(); SleuthkitCase sleuthkitCase = currentCase.getSleuthkitCase(); - + String selectStatement = this.buildSqlSelectStatement(); try ( @@ -127,21 +127,21 @@ public abstract class IntraCaseCommonAttributeSearcher extends AbstractCommonAtt } } } - + Map instanceCollatedCommonFiles = collateMatchesByNumberOfInstances(commonFiles); - + return new CommonAttributeSearchResults(instanceCollatedCommonFiles, this.frequencyPercentageThreshold); } /** - * Should be used by subclasses, in their - * buildSqlSelectStatement() function to create an SQL boolean - * expression which will filter our matches based on mime type. The + * Should be used by subclasses, in their + * buildSqlSelectStatement() function to create an SQL boolean + * expression which will filter our matches based on mime type. The * expression will be conjoined to base query with an AND operator. - * - * @return sql fragment of the form: - * 'and "mime_type" in ( [comma delimited list of mime types] )' - * or empty string in the event that no types to filter on were given. + * + * @return sql fragment of the form: 'and "mime_type" in ( [comma delimited + * list of mime types] )' or empty string in the event that no types to + * filter on were given. */ String determineMimeTypeFilter() { diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/IntraCasePanel.form b/Core/src/org/sleuthkit/autopsy/commonfilesearch/IntraCasePanel.form index 7038ca5926..be311f3e46 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/IntraCasePanel.form +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/IntraCasePanel.form @@ -23,62 +23,58 @@ - - - - - + + - - - + + + + + + + + + + + + + + + + + + + - + + - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -86,12 +82,93 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/IntraCasePanel.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/IntraCasePanel.java index babbbd4096..4841d39e84 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/IntraCasePanel.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/IntraCasePanel.java @@ -1,16 +1,16 @@ /* - * + * * Autopsy Forensic Browser - * + * * Copyright 2018 Basis Technology Corp. * Contact: carrier sleuthkit org - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -23,19 +23,21 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; +import java.util.Observable; +import java.util.Observer; import javax.swing.ComboBoxModel; + /** * UI controls for Common Files Search scenario where the user intends to find - * common files between datasources. It is an inner panel which provides the ability - * to select all datasources or a single datasource from a dropdown list of - * sources in the current case. + * common files between datasources. It is an inner panel which provides the + * ability to select all datasources or a single datasource from a dropdown list + * of sources in the current case. */ -public class IntraCasePanel extends javax.swing.JPanel { - +public final class IntraCasePanel extends javax.swing.JPanel { + private static final long serialVersionUID = 1L; static final long NO_DATA_SOURCE_SELECTED = -1; - - private boolean singleDataSource; + private final Observable fileTypeFilterObservable; private ComboBoxModel dataSourcesList = new DataSourceComboBoxModel(); private final Map dataSourceMap; @@ -45,26 +47,86 @@ public class IntraCasePanel extends javax.swing.JPanel { public IntraCasePanel() { initComponents(); this.dataSourceMap = new HashMap<>(); - this.singleDataSource = true; + this.onlySpecificDataSourceCheckbox.setEnabled(true); + fileTypeFilterObservable = new Observable() { + @Override + public void notifyObservers() { + //set changed before notify observers + //we want this observerable to always cause the observer to update when notified + this.setChanged(); + super.notifyObservers(); + } + }; } - - public Map getDataSourceMap(){ + + /** + * Add an Observer to the Observable portion of this panel so that it can be + * notified of changes to this panel. + * + * @param observer the object which is observing this panel + */ + void addObserver(Observer observer) { + fileTypeFilterObservable.addObserver(observer); + } + + /** + * Get the map of datasources which was used to populate the combo box on + * this panel. + * + * @return an unmodifiable copy of the map of datasources + */ + Map getDataSourceMap() { return Collections.unmodifiableMap(this.dataSourceMap); } - - Long getSelectedDataSourceId(){ - if(!this.singleDataSource){ - return IntraCasePanel.NO_DATA_SOURCE_SELECTED; - } - - for(Entry entry : this.dataSourceMap.entrySet()){ - if(entry.getValue().equals(this.selectDataSourceComboBox.getSelectedItem())){ - return entry.getKey(); + + /** + * Get the ID for the datasource selected in the combo box. + * + * @return the selected datasource ID or + * IntraCasePanel.NO_DATA_SOURCE_SELECTED if none is selected + */ + Long getSelectedDataSourceId() { + if (onlySpecificDataSourceCheckbox.isSelected()) { + for (Entry entry : this.dataSourceMap.entrySet()) { + if (entry.getValue().equals(this.selectDataSourceComboBox.getSelectedItem())) { + return entry.getKey(); + } } } - return IntraCasePanel.NO_DATA_SOURCE_SELECTED; - } + } + + /** + * If the user has selected to show only results of specific file types. + * + * @return if the selected file categories button is selected, true for + * selected false for not selected + */ + boolean fileCategoriesButtonIsSelected() { + return selectedFileCategoriesButton.isSelected(); + } + + /** + * If the user has selected selected to show Picture and Video files as part + * of the filtered results. + * + * @return if the pictures and video checkbox is enabled AND selected, true + * for enabled AND selected false for not selected OR not enabled + */ + boolean pictureVideoCheckboxIsSelected() { + return pictureVideoCheckbox.isEnabled() && pictureVideoCheckbox.isSelected(); + } + + /** + * If the user has selected selected to show Document files as part of the + * filtered results. + * + * @return if the documents checkbox is enabled AND selected, true for + * enabled AND selected false for not selected OR not enabled + */ + boolean documentsCheckboxIsSelected() { + return documentsCheckbox.isEnabled() && documentsCheckbox.isSelected(); + } /** * This method is called from within the constructor to initialize the form. @@ -76,32 +138,67 @@ public class IntraCasePanel extends javax.swing.JPanel { private void initComponents() { buttonGroup = new javax.swing.ButtonGroup(); - allDataSourcesRadioButton = new javax.swing.JRadioButton(); - withinDataSourceRadioButton = new javax.swing.JRadioButton(); selectDataSourceComboBox = new javax.swing.JComboBox<>(); - - buttonGroup.add(allDataSourcesRadioButton); - org.openide.awt.Mnemonics.setLocalizedText(allDataSourcesRadioButton, org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.allDataSourcesRadioButton.text")); // NOI18N - allDataSourcesRadioButton.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); - allDataSourcesRadioButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - allDataSourcesRadioButtonActionPerformed(evt); - } - }); - - buttonGroup.add(withinDataSourceRadioButton); - org.openide.awt.Mnemonics.setLocalizedText(withinDataSourceRadioButton, org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.withinDataSourceRadioButton.text")); // NOI18N - withinDataSourceRadioButton.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); - withinDataSourceRadioButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - withinDataSourceRadioButtonActionPerformed(evt); - } - }); + categoriesLabel = new javax.swing.JLabel(); + selectedFileCategoriesButton = new javax.swing.JRadioButton(); + pictureVideoCheckbox = new javax.swing.JCheckBox(); + documentsCheckbox = new javax.swing.JCheckBox(); + allFileCategoriesRadioButton = new javax.swing.JRadioButton(); + onlySpecificDataSourceCheckbox = new javax.swing.JCheckBox(); selectDataSourceComboBox.setModel(dataSourcesList); - selectDataSourceComboBox.setActionCommand(org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.selectDataSourceComboBox.actionCommand")); // NOI18N selectDataSourceComboBox.setEnabled(false); + org.openide.awt.Mnemonics.setLocalizedText(categoriesLabel, org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.categoriesLabel.text")); // NOI18N + categoriesLabel.setName(""); // NOI18N + + buttonGroup.add(selectedFileCategoriesButton); + org.openide.awt.Mnemonics.setLocalizedText(selectedFileCategoriesButton, org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.selectedFileCategoriesButton.text")); // NOI18N + selectedFileCategoriesButton.setToolTipText(org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.selectedFileCategoriesButton.toolTipText")); // NOI18N + selectedFileCategoriesButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + selectedFileCategoriesButtonActionPerformed(evt); + } + }); + + pictureVideoCheckbox.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(pictureVideoCheckbox, org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.pictureVideoCheckbox.text")); // NOI18N + pictureVideoCheckbox.setEnabled(false); + pictureVideoCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + pictureVideoCheckboxActionPerformed(evt); + } + }); + + documentsCheckbox.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(documentsCheckbox, org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.documentsCheckbox.text")); // NOI18N + documentsCheckbox.setEnabled(false); + documentsCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + documentsCheckboxActionPerformed(evt); + } + }); + + buttonGroup.add(allFileCategoriesRadioButton); + allFileCategoriesRadioButton.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(allFileCategoriesRadioButton, org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.allFileCategoriesRadioButton.text")); // NOI18N + allFileCategoriesRadioButton.setToolTipText(org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.allFileCategoriesRadioButton.toolTipText")); // NOI18N + allFileCategoriesRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + allFileCategoriesRadioButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(onlySpecificDataSourceCheckbox, org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.onlySpecificDataSourceCheckbox.text")); // NOI18N + onlySpecificDataSourceCheckbox.setMaximumSize(new java.awt.Dimension(243, 23)); + onlySpecificDataSourceCheckbox.setMinimumSize(new java.awt.Dimension(243, 23)); + onlySpecificDataSourceCheckbox.setPreferredSize(new java.awt.Dimension(243, 23)); + onlySpecificDataSourceCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + onlySpecificDataSourceCheckboxActionPerformed(evt); + } + }); + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( @@ -109,65 +206,111 @@ public class IntraCasePanel extends javax.swing.JPanel { .addGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(allDataSourcesRadioButton) - .addComponent(withinDataSourceRadioButton))) + .addGap(21, 21, 21) + .addComponent(selectDataSourceComboBox, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) .addGroup(layout.createSequentialGroup() - .addGap(27, 27, 27) - .addComponent(selectDataSourceComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 261, javax.swing.GroupLayout.PREFERRED_SIZE))) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(categoriesLabel) + .addGroup(layout.createSequentialGroup() + .addGap(19, 19, 19) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(allFileCategoriesRadioButton) + .addComponent(selectedFileCategoriesButton) + .addGroup(layout.createSequentialGroup() + .addGap(21, 21, 21) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(documentsCheckbox) + .addComponent(pictureVideoCheckbox)))))) + .addGap(0, 0, Short.MAX_VALUE))) + .addContainerGap()) + .addComponent(onlySpecificDataSourceCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, 384, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() - .addComponent(allDataSourcesRadioButton) + .addContainerGap() + .addComponent(onlySpecificDataSourceCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(withinDataSourceRadioButton) + .addComponent(selectDataSourceComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(categoriesLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(selectDataSourceComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(allFileCategoriesRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(selectedFileCategoriesButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(pictureVideoCheckbox) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(documentsCheckbox) + .addContainerGap()) ); }// //GEN-END:initComponents - private void allDataSourcesRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_allDataSourcesRadioButtonActionPerformed - selectDataSourceComboBox.setEnabled(!allDataSourcesRadioButton.isSelected()); - singleDataSource = false; - }//GEN-LAST:event_allDataSourcesRadioButtonActionPerformed + private void selectedFileCategoriesButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_selectedFileCategoriesButtonActionPerformed + //When the selectedFileCategoriesButton is selected enable its related options + //and notify observers that the panel has changed incase the current settings are invalid + pictureVideoCheckbox.setEnabled(true); + documentsCheckbox.setEnabled(true); + fileTypeFilterObservable.notifyObservers(); + }//GEN-LAST:event_selectedFileCategoriesButtonActionPerformed - private void withinDataSourceRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_withinDataSourceRadioButtonActionPerformed - withinDataSourceSelected(withinDataSourceRadioButton.isSelected()); - }//GEN-LAST:event_withinDataSourceRadioButtonActionPerformed + private void allFileCategoriesRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_allFileCategoriesRadioButtonActionPerformed + //When the allFileCategoriesRadioButton is selected disable the options + //related to selected file categories and notify observers that the panel has changed + //incase the current settings are invalid + pictureVideoCheckbox.setEnabled(false); + documentsCheckbox.setEnabled(false); + fileTypeFilterObservable.notifyObservers(); + }//GEN-LAST:event_allFileCategoriesRadioButtonActionPerformed - private void withinDataSourceSelected(boolean selected) { - selectDataSourceComboBox.setEnabled(selected); + private void onlySpecificDataSourceCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_onlySpecificDataSourceCheckboxActionPerformed + //When the onlySpecificDataSourceCheckbox is clicked update its related options + selectDataSourceComboBox.setEnabled(onlySpecificDataSourceCheckbox.isSelected()); if (selectDataSourceComboBox.isEnabled()) { selectDataSourceComboBox.setSelectedIndex(0); - singleDataSource = true; } - } + }//GEN-LAST:event_onlySpecificDataSourceCheckboxActionPerformed + + private void pictureVideoCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_pictureVideoCheckboxActionPerformed + //notify observers that the panel has changed incase the current settings are invalid + fileTypeFilterObservable.notifyObservers(); + }//GEN-LAST:event_pictureVideoCheckboxActionPerformed + + private void documentsCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_documentsCheckboxActionPerformed + //notify observers that the panel has changed incase the current settings are invalid + fileTypeFilterObservable.notifyObservers(); + }//GEN-LAST:event_documentsCheckboxActionPerformed // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JRadioButton allDataSourcesRadioButton; + private javax.swing.JRadioButton allFileCategoriesRadioButton; private javax.swing.ButtonGroup buttonGroup; + private javax.swing.JLabel categoriesLabel; + private javax.swing.JCheckBox documentsCheckbox; + private javax.swing.JCheckBox onlySpecificDataSourceCheckbox; + private javax.swing.JCheckBox pictureVideoCheckbox; private javax.swing.JComboBox selectDataSourceComboBox; - private javax.swing.JRadioButton withinDataSourceRadioButton; + private javax.swing.JRadioButton selectedFileCategoriesButton; // End of variables declaration//GEN-END:variables - void setDataModel(DataSourceComboBoxModel dataSourceComboBoxModel) { + /** + * Set the datamodel for the combo box which displays the data sources in + * the current case + * + * @param dataSourceComboBoxModel the DataSourceComboBoxModel to use + */ + void setDatasourceComboboxModel(DataSourceComboBoxModel dataSourceComboBoxModel) { this.dataSourcesList = dataSourceComboBoxModel; this.selectDataSourceComboBox.setModel(dataSourcesList); } - void rigForMultipleDataSources(boolean multipleDataSources) { - this.withinDataSourceRadioButton.setEnabled(multipleDataSources); - this.allDataSourcesRadioButton.setSelected(!multipleDataSources); - this.withinDataSourceRadioButton.setSelected(multipleDataSources); - this.withinDataSourceSelected(multipleDataSources); - - } - + /** + * Update the map of datasources that this panel allows the user to select + * from + * + * @param dataSourceMap A map of datasources + */ void setDataSourceMap(Map dataSourceMap) { this.dataSourceMap.clear(); this.dataSourceMap.putAll(dataSourceMap); } -} \ No newline at end of file +} diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/SingleInterCaseCommonAttributeSearcher.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/SingleInterCaseCommonAttributeSearcher.java index 5bed8625a0..3d2abda13c 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/SingleInterCaseCommonAttributeSearcher.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/SingleInterCaseCommonAttributeSearcher.java @@ -1,16 +1,16 @@ /* - * + * * Autopsy Forensic Browser - * + * * Copyright 2018 Basis Technology Corp. * Contact: carrier sleuthkit org - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,67 +20,77 @@ package org.sleuthkit.autopsy.commonfilesearch; import java.sql.SQLException; -import java.util.List; import java.util.Map; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationCase; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance.Type; /** - * - * + * + * */ public class SingleInterCaseCommonAttributeSearcher extends InterCaseCommonAttributeSearcher { - + private final int corrleationCaseId; private String correlationCaseName; - + /** - * + * * @param correlationCaseId * @param filterByMediaMimeType * @param filterByDocMimeType - * @throws EamDbException + * + * @throws EamDbException */ - public SingleInterCaseCommonAttributeSearcher(int correlationCaseId, Map dataSourceIdMap, boolean filterByMediaMimeType, boolean filterByDocMimeType, int percentageThreshold) throws EamDbException { - super(dataSourceIdMap,filterByMediaMimeType, filterByDocMimeType, percentageThreshold); - + public SingleInterCaseCommonAttributeSearcher(int correlationCaseId, Map dataSourceIdMap, boolean filterByMediaMimeType, + boolean filterByDocMimeType, Type corAttrType, int percentageThreshold) throws EamDbException { + super(dataSourceIdMap, filterByMediaMimeType, filterByDocMimeType, corAttrType, percentageThreshold); + this.corrleationCaseId = correlationCaseId; this.correlationCaseName = ""; } - + /** - * Collect metadata required to render the tree table where matches must + * Collect metadata required to render the tree table where matches must * occur in the case with the given ID. - * - * @param correlationCaseId id of case where matches must occur (no other matches will be shown) + * + * @param correlationCaseId id of case where matches must occur (no other + * matches will be shown) + * * @return business object needed to populate tree table with results + * * @throws TskCoreException * @throws NoCurrentCaseException * @throws SQLException * @throws EamDbException */ @Override - public CommonAttributeSearchResults findFiles() throws TskCoreException, NoCurrentCaseException, SQLException, EamDbException { - + public CommonAttributeSearchResults findMatches() throws TskCoreException, NoCurrentCaseException, SQLException, EamDbException { + CorrelationCase cCase = this.getCorrelationCaseFromId(this.corrleationCaseId); - correlationCaseName = cCase.getDisplayName(); + this.correlationCaseName = cCase.getDisplayName(); return this.findFiles(cCase); } CommonAttributeSearchResults findFiles(CorrelationCase correlationCase) throws TskCoreException, NoCurrentCaseException, SQLException, EamDbException { - InterCaseSearchResultsProcessor eamDbAttrInst = new InterCaseSearchResultsProcessor(this.getDataSourceIdToNameMap()); + InterCaseSearchResultsProcessor eamDbAttrInst = new InterCaseSearchResultsProcessor(this.getDataSourceIdToNameMap(), this.corAttrType); Map interCaseCommonFiles = eamDbAttrInst.findSingleInterCaseCommonAttributeValues(Case.getCurrentCase(), correlationCase); - return new CommonAttributeSearchResults(interCaseCommonFiles, this.frequencyPercentageThreshold); + return new CommonAttributeSearchResults(interCaseCommonFiles, this.frequencyPercentageThreshold, this.corAttrType); } - + + @NbBundle.Messages({ + "# {0} - case name", + "# {1} - attr type", + "# {2} - threshold string", + "SingleInterCaseCommonAttributeSearcher.buildTabTitle.titleInterSingle=Common Properties (Central Repository Case: {0}, {1}{2})"}) + @Override - String buildTabTitle() { - final String buildCategorySelectionString = this.buildCategorySelectionString(); - final String titleTemplate = Bundle.AbstractCommonFilesMetadataBuilder_buildTabTitle_titleInterSingle(); - return String.format(titleTemplate, new Object[]{correlationCaseName, buildCategorySelectionString}); + String getTabTitle() { + return Bundle.SingleInterCaseCommonAttributeSearcher_buildTabTitle_titleInterSingle(this.correlationCaseName, this.corAttrType.getDisplayName(), this.getPercentThresholdString()); } -} \ No newline at end of file +} diff --git a/Core/src/org/sleuthkit/autopsy/commonfilesearch/SingleIntraCaseCommonAttributeSearcher.java b/Core/src/org/sleuthkit/autopsy/commonfilesearch/SingleIntraCaseCommonAttributeSearcher.java index f28f0d0bf0..cfbdf10c59 100644 --- a/Core/src/org/sleuthkit/autopsy/commonfilesearch/SingleIntraCaseCommonAttributeSearcher.java +++ b/Core/src/org/sleuthkit/autopsy/commonfilesearch/SingleIntraCaseCommonAttributeSearcher.java @@ -1,16 +1,16 @@ /* - * + * * Autopsy Forensic Browser - * + * * Copyright 2018 Basis Technology Corp. * Contact: carrier sleuthkit org - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,6 +20,7 @@ package org.sleuthkit.autopsy.commonfilesearch; import java.util.Map; +import org.openide.util.NbBundle; import org.sleuthkit.datamodel.TskData.FileKnown; /** @@ -27,19 +28,21 @@ import org.sleuthkit.datamodel.TskData.FileKnown; */ final public class SingleIntraCaseCommonAttributeSearcher extends IntraCaseCommonAttributeSearcher { - private static final String WHERE_CLAUSE = "%s md5 in (select md5 from tsk_files where md5 in (select md5 from tsk_files where (known != "+ FileKnown.KNOWN.getFileKnownValue() + " OR known IS NULL) and data_source_obj_id=%s%s) GROUP BY md5 HAVING COUNT(DISTINCT data_source_obj_id) > 1) order by md5"; //NON-NLS + private static final String WHERE_CLAUSE = "%s md5 in (select md5 from tsk_files where md5 in (select md5 from tsk_files where (known != " + FileKnown.KNOWN.getFileKnownValue() + " OR known IS NULL) and data_source_obj_id=%s%s) GROUP BY md5 HAVING COUNT(DISTINCT data_source_obj_id) > 1) order by md5"; //NON-NLS private final Long selectedDataSourceId; private final String dataSourceName; /** * Implements the algorithm for getting common files that appear at least * once in the given data source - * @param dataSourceId data source id for which common files must appear at least once - * @param dataSourceIdMap a map of obj_id to datasource name + * + * @param dataSourceId data source id for which common files must + * appear at least once + * @param dataSourceIdMap a map of obj_id to datasource name * @param filterByMediaMimeType match only on files whose mime types can be - * broadly categorized as media types - * @param filterByDocMimeType match only on files whose mime types can be - * broadly categorized as document types + * broadly categorized as media types + * @param filterByDocMimeType match only on files whose mime types can be + * broadly categorized as document types */ public SingleIntraCaseCommonAttributeSearcher(Long dataSourceId, Map dataSourceIdMap, boolean filterByMediaMimeType, boolean filterByDocMimeType, int percentageThreshold) { super(dataSourceIdMap, filterByMediaMimeType, filterByDocMimeType, percentageThreshold); @@ -53,10 +56,13 @@ final public class SingleIntraCaseCommonAttributeSearcher extends IntraCaseCommo return String.format(SingleIntraCaseCommonAttributeSearcher.WHERE_CLAUSE, args); } + @NbBundle.Messages({ + "# {0} - data source name", + "# {1} - build category", + "# {2} - threshold string", + "SingleIntraCaseCommonAttributeSearcher.buildTabTitle.titleIntraSingle=Common Properties (Data Source: {0}, {1}{2})"}) @Override - public String buildTabTitle() { - final String buildCategorySelectionString = this.buildCategorySelectionString(); - final String titleTemplate = Bundle.AbstractCommonFilesMetadataBuilder_buildTabTitle_titleIntraSingle(); - return String.format(titleTemplate, new Object[]{this.dataSourceName, buildCategorySelectionString}); + String getTabTitle() { + return Bundle.SingleIntraCaseCommonAttributeSearcher_buildTabTitle_titleIntraSingle(this.dataSourceName, this.buildCategorySelectionString(), this.getPercentThresholdString()); } -} \ No newline at end of file +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/RelationshipNode.java b/Core/src/org/sleuthkit/autopsy/communications/RelationshipNode.java index 2fd30addcc..0b92830002 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/RelationshipNode.java +++ b/Core/src/org/sleuthkit/autopsy/communications/RelationshipNode.java @@ -18,10 +18,12 @@ */ package org.sleuthkit.autopsy.communications; +import java.util.List; import java.util.TimeZone; import java.util.logging.Level; import org.apache.commons.lang3.StringUtils; import org.openide.nodes.Sheet; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.BlackboardArtifactNode; import org.sleuthkit.autopsy.datamodel.NodeProperty; @@ -37,6 +39,7 @@ import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHO import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SUBJECT; import static org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME; +import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TimeUtilities; import org.sleuthkit.datamodel.TskCoreException; @@ -57,6 +60,7 @@ final class RelationshipNode extends BlackboardArtifactNode { @Override protected Sheet createSheet() { Sheet sheet = new Sheet(); + List tags = getAllTagsFromDatabase(); Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES); if (sheetSet == null) { sheetSet = Sheet.createPropertiesSet(); @@ -64,7 +68,10 @@ final class RelationshipNode extends BlackboardArtifactNode { } sheetSet.put(new NodeProperty<>("Type", "Type", "Type", getDisplayName())); - + CorrelationAttributeInstance correlationAttribute = getCorrelationAttributeInstance(); + addScoreProperty(sheetSet, tags); + addCommentProperty(sheetSet, tags, correlationAttribute); + addCountProperty(sheetSet, correlationAttribute); final BlackboardArtifact artifact = getArtifact(); BlackboardArtifact.ARTIFACT_TYPE fromID = BlackboardArtifact.ARTIFACT_TYPE.fromID(getArtifact().getArtifactTypeID()); if (null != fromID) { @@ -113,8 +120,7 @@ final class RelationshipNode extends BlackboardArtifactNode { break; } } - - addTagProperty(sheetSet); + addTagProperty(sheetSet, tags); return sheet; } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.form b/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.form new file mode 100755 index 0000000000..4fe61b2fb5 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.form @@ -0,0 +1,50 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.java new file mode 100755 index 0000000000..f4f1d0380c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.java @@ -0,0 +1,466 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers; + +import java.awt.Component; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import org.apache.commons.lang3.StringEscapeUtils; + +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.openide.nodes.Node; +import org.openide.util.lookup.ServiceProvider; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationCase; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationDataSource; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamArtifactUtil; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardArtifactTag; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.Tag; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Annotations view of file contents. + */ +@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives +@ServiceProvider(service = DataContentViewer.class, position = 8) +@NbBundle.Messages({ + "AnnotationsContentViewer.title=Annotations", + "AnnotationsContentViewer.toolTip=Displays tags and comments associated with the selected content." +}) +public class AnnotationsContentViewer extends javax.swing.JPanel implements DataContentViewer { + + private static final Logger logger = Logger.getLogger(AnnotationsContentViewer.class.getName()); + + /** + * Creates an instance of AnnotationsContentViewer. + */ + public AnnotationsContentViewer() { + initComponents(); + Utilities.configureTextPaneAsHtml(jTextPane1); + } + + @Override + public void setNode(Node node) { + if ((node == null) || (!isSupported(node))) { + resetComponent(); + return; + } + + StringBuilder html = new StringBuilder(); + + BlackboardArtifact artifact = node.getLookup().lookup(BlackboardArtifact.class); + Content sourceFile = null; + + try { + if (artifact != null) { + /* + * Get the source content based on the artifact to ensure we + * display the correct data instead of whatever was in the node. + */ + sourceFile = artifact.getSleuthkitCase().getAbstractFileById(artifact.getObjectID()); + } else { + /* + * No artifact is present, so get the content based on what's + * present in the node. In this case, the selected item IS the + * source file. + */ + sourceFile = node.getLookup().lookup(AbstractFile.class); + } + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, String.format( + "Exception while trying to retrieve a Content instance from the BlackboardArtifact '%s' (id=%d).", + artifact.getDisplayName(), artifact.getArtifactID()), ex); + } + + if (artifact != null) { + populateTagData(html, artifact, sourceFile); + } else { + populateTagData(html, sourceFile); + } + + if (sourceFile instanceof AbstractFile) { + populateCentralRepositoryData(html, artifact, (AbstractFile) sourceFile); + } + + setText(html.toString()); + jTextPane1.setCaretPosition(0); + } + + /** + * Populate the "Selected Item" sections with tag data for the supplied + * content. + * + * @param html The HTML text to update. + * @param content Selected content. + */ + private void populateTagData(StringBuilder html, Content content) { + try { + SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase(); + + startSection(html, "Selected Item"); + List fileTagsList = tskCase.getContentTagsByContent(content); + if (fileTagsList.isEmpty()) { + addMessage(html, "There are no tags for the selected content."); + } else { + for (ContentTag tag : fileTagsList) { + addTagEntry(html, tag); + } + } + endSection(html); + } catch (NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS + } + } + + /** + * Populate the "Selected Item" and "Source File" sections with tag data for + * a supplied artifact. + * + * @param html The HTML text to update. + * @param artifact A selected artifact. + * @param sourceFile The source content of the selected artifact. + */ + private void populateTagData(StringBuilder html, BlackboardArtifact artifact, Content sourceFile) { + try { + SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase(); + + startSection(html, "Selected Item"); + List artifactTagsList = tskCase.getBlackboardArtifactTagsByArtifact(artifact); + if (artifactTagsList.isEmpty()) { + addMessage(html, "There are no tags for the selected artifact."); + } else { + for (BlackboardArtifactTag tag : artifactTagsList) { + addTagEntry(html, tag); + } + } + endSection(html); + + if (sourceFile != null) { + startSection(html, "Source File"); + List fileTagsList = tskCase.getContentTagsByContent(sourceFile); + if (fileTagsList.isEmpty()) { + addMessage(html, "There are no tags for the source content."); + } else { + for (ContentTag tag : fileTagsList) { + addTagEntry(html, tag); + } + } + endSection(html); + } + } catch (NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS + } + } + + /** + * Populate the "Central Repository Comments" section with data. + * + * @param html The HTML text to update. + * @param artifact A selected artifact (can be null). + * @param sourceFile A selected file, or a source file of the selected + * artifact. + */ + private void populateCentralRepositoryData(StringBuilder html, BlackboardArtifact artifact, AbstractFile sourceFile) { + if (EamDb.isEnabled()) { + startSection(html, "Central Repository Comments"); + List instancesList = new ArrayList<>(); + if (artifact != null) { + instancesList.addAll(EamArtifactUtil.makeInstancesFromBlackboardArtifact(artifact, false)); + } + try { + List artifactTypes = EamDb.getInstance().getDefinedCorrelationTypes(); + String md5 = sourceFile.getMd5Hash(); + if (md5 != null && !md5.isEmpty() && null != artifactTypes && !artifactTypes.isEmpty()) { + for (CorrelationAttributeInstance.Type attributeType : artifactTypes) { + if (attributeType.getId() == CorrelationAttributeInstance.FILES_TYPE_ID) { + CorrelationCase correlationCase = EamDb.getInstance().getCase(Case.getCurrentCase()); + instancesList.add(new CorrelationAttributeInstance( + md5, + attributeType, + correlationCase, + CorrelationDataSource.fromTSKDataSource(correlationCase, sourceFile.getDataSource()), + sourceFile.getParentPath() + sourceFile.getName(), + "", + sourceFile.getKnown())); + break; + } + } + } + + boolean commentDataFound = false; + + for (CorrelationAttributeInstance instance : instancesList) { + List correlatedInstancesList = + EamDb.getInstance().getArtifactInstancesByTypeValue(instance.getCorrelationType(), instance.getCorrelationValue()); + for (CorrelationAttributeInstance correlatedInstance : correlatedInstancesList) { + if (correlatedInstance.getComment() != null && correlatedInstance.getComment().isEmpty() == false) { + commentDataFound = true; + addCentralRepositoryEntry(html, correlatedInstance); + } + } + } + + if (commentDataFound == false) { + addMessage(html, "There is no comment data for the selected content in the central repository."); + } + } catch (EamDbException | TskCoreException ex) { + logger.log(Level.SEVERE, "Error connecting to the central repository database.", ex); // NON-NLS + } catch (CorrelationAttributeNormalizationException ex) { + logger.log(Level.SEVERE, "Error normalizing instance from repository database.", ex); // NON-NLS + } + endSection(html); + } + } + + /** + * Set the text of the text panel. + * + * @param text The text to set to the text panel. + */ + private void setText(String text) { + jTextPane1.setText("" + text + ""); //NON-NLS + } + + /** + * Start a new data section. + * + * @param html The HTML text to add the section to. + * @param sectionName The name of the section. + */ + private void startSection(StringBuilder html, String sectionName) { + html.append("

") + .append(sectionName) + .append("


"); //NON-NLS + } + + /** + * Add a message. + * + * @param html The HTML text to add the message to. + * @param message The message text. + */ + private void addMessage(StringBuilder html, String message) { + html.append("

") + .append(message) + .append("


"); //NON-NLS + } + + /** + * Add a data table containing information about a tag. + * + * @param html The HTML text to add the table to. + * @param tag The tag whose information will be used to populate the table. + */ + @NbBundle.Messages({ + "AnnotationsContentViewer.tagEntryDataLabel.tag=Tag:", + "AnnotationsContentViewer.tagEntryDataLabel.tagUser=Tag User:", + "AnnotationsContentViewer.tagEntryDataLabel.comment=Comment:" + }) + private void addTagEntry(StringBuilder html, Tag tag) { + startTable(html); + addRow(html, Bundle.AnnotationsContentViewer_tagEntryDataLabel_tag(), tag.getName().getDisplayName()); + addRow(html, Bundle.AnnotationsContentViewer_tagEntryDataLabel_tagUser(), tag.getUserName()); + addRow(html, Bundle.AnnotationsContentViewer_tagEntryDataLabel_comment(), formatHtmlString(tag.getComment())); + endTable(html); + } + + /** + * Add a data table containing information about a correlation attribute + * instance in the central repository. + * + * @param html The HTML text to add the table to. + * @param attributeInstance The attribute instance whose information will be + * used to populate the table. + * @param correlationType The correlation data type. + */ + @NbBundle.Messages({ + "AnnotationsContentViewer.centralRepositoryEntryDataLabel.case=Case:", + "AnnotationsContentViewer.centralRepositoryEntryDataLabel.type=Type:", + "AnnotationsContentViewer.centralRepositoryEntryDataLabel.comment=Comment:", + "AnnotationsContentViewer.centralRepositoryEntryDataLabel.path=Path:" + }) + private void addCentralRepositoryEntry(StringBuilder html, CorrelationAttributeInstance attributeInstance) { + startTable(html); + addRow(html, Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_case(), attributeInstance.getCorrelationCase().getDisplayName()); + addRow(html, Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_type(), attributeInstance.getCorrelationType().getDisplayName()); + addRow(html, Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_comment(), formatHtmlString(attributeInstance.getComment())); + addRow(html, Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_path(), attributeInstance.getFilePath()); + endTable(html); + } + + /** + * Start a data table. + * + * @param html The HTML text to add the table to. + */ + private void startTable(StringBuilder html) { + html.append(""); //NON-NLS + } + + /** + * Add a data row to a table. + * + * @param html The HTML text to add the row to. + * @param key The key for the left column of the data row. + * @param value The value for the right column of the data row. + */ + private void addRow(StringBuilder html, String key, String value) { + html.append(""); //NON-NLS + } + + /** + * End a data table. + * + * @param html The HTML text on which to end a table. + */ + private void endTable(StringBuilder html) { + html.append("
"); //NON-NLS + html.append(key); + html.append(""); //NON-NLS + html.append(value); + html.append("


"); //NON-NLS + } + + /** + * End a data section. + * + * @param html The HTML text on which to end a section. + */ + private void endSection(StringBuilder html) { + html.append("
"); //NON-NLS + } + + /** + * Apply escape sequence to special characters. Line feed and carriage + * return character combinations will be converted to HTML line breaks. + * + * @param text The text to format. + * @return The formatted text. + */ + private String formatHtmlString(String text) { + String formattedString = StringEscapeUtils.escapeHtml4(text); + return formattedString.replaceAll("(\r\n|\r|\n|\n\r)", "
"); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + jScrollPane5 = new javax.swing.JScrollPane(); + jTextPane1 = new javax.swing.JTextPane(); + + setPreferredSize(new java.awt.Dimension(100, 58)); + + jTextPane1.setEditable(false); + jTextPane1.setName(""); // NOI18N + jTextPane1.setPreferredSize(new java.awt.Dimension(600, 52)); + jScrollPane5.setViewportView(jTextPane1); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jScrollPane5, javax.swing.GroupLayout.DEFAULT_SIZE, 907, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jScrollPane5, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 435, Short.MAX_VALUE) + ); + }// //GEN-END:initComponents + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JScrollPane jScrollPane5; + private javax.swing.JTextPane jTextPane1; + // End of variables declaration//GEN-END:variables + + @Override + public String getTitle() { + return Bundle.AnnotationsContentViewer_title(); + } + + @Override + public String getToolTip() { + return Bundle.AnnotationsContentViewer_toolTip(); + } + + @Override + public DataContentViewer createInstance() { + return new AnnotationsContentViewer(); + } + + @Override + public boolean isSupported(Node node) { + BlackboardArtifact artifact = node.getLookup().lookup(BlackboardArtifact.class); + + try { + if (artifact != null) { + if (artifact.getSleuthkitCase().getAbstractFileById(artifact.getObjectID()) != null) { + return true; + } + } else { + if (node.getLookup().lookup(AbstractFile.class) != null) { + return true; + } + } + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, String.format( + "Exception while trying to retrieve a Content instance from the BlackboardArtifact '%s' (id=%d).", + artifact.getDisplayName(), artifact.getArtifactID()), ex); + } + + return false; + } + + @Override + public int isPreferred(Node node) { + return 1; + } + + @Override + public Component getComponent() { + return this; + } + + @Override + public void resetComponent() { + setText(""); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle_ja.properties index 38f772cbf0..b033af82f9 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle_ja.properties @@ -46,4 +46,4 @@ Metadata.toolTip=\u30d5\u30a1\u30a4\u30eb\u306e\u30e1\u30bf\u30c7\u30fc\u30bf\u3 Metadata.tableRowTitle.type=\u30bf\u30a4\u30d7 Metadata.nodeText.exceptionNotice.text=\u30d5\u30a1\u30a4\u30eb\u30e1\u30bf\u30c7\u30fc\u30bf\u3092\u53d6\u5f97\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\uff1a Metadata.nodeText.text=Sleuth Kit istat\u30c4\u30fc\u30eb\u304b\u3089\uff1a -Metadata.nodeText.nonFilePassedIn=\u5165\u529b\u3055\u308c\u305f\u3082\u306e\u306f\u30d5\u30a1\u30a4\u30eb\u3067\u306f\u3042\u308a\u307e\u305b\u3093 \ No newline at end of file +Metadata.nodeText.nonFilePassedIn=\u5165\u529b\u3055\u308c\u305f\u3082\u306e\u306f\u30d5\u30a1\u30a4\u30eb\u3067\u306f\u3042\u308a\u307e\u305b\u3093 diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.java index a95dd307f5..b109b12da0 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.java @@ -36,6 +36,7 @@ import org.openide.nodes.Node; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.lookup.ServiceProvider; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer; import org.sleuthkit.autopsy.corecomponents.DataResultPanel; import org.sleuthkit.autopsy.corecomponents.TableFilterNode; @@ -65,6 +66,7 @@ import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHO import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SUBJECT; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TEXT; +import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.TskCoreException; /** @@ -723,13 +725,19 @@ public class MessageContentViewer extends javax.swing.JPanel implements DataCont sheetSet = Sheet.createPropertiesSet(); sheet.put(sheetSet); } + List tags = getContentTagsFromDatabase(); + AbstractFile file = getContent(); sheetSet.put(new NodeProperty<>("Name", "Name", "Name", file.getName())); + CorrelationAttributeInstance correlationAttribute = getCorrelationAttributeInstance(); + addScoreProperty(sheetSet, tags); + addCommentProperty(sheetSet, tags, correlationAttribute); + addCountProperty(sheetSet, correlationAttribute); sheetSet.put(new NodeProperty<>("Size", "Size", "Size", file.getSize())); sheetSet.put(new NodeProperty<>("Mime Type", "Mime Type", "Mime Type", StringUtils.defaultString(file.getMIMEType()))); sheetSet.put(new NodeProperty<>("Known", "Known", "Known", file.getKnown().getName())); - addTagProperty(sheetSet); + addTagProperty(sheetSet, tags); return sheet; } } diff --git a/Core/src/org/sleuthkit/autopsy/core/AutopsyOptionProcessor.java b/Core/src/org/sleuthkit/autopsy/core/AutopsyOptionProcessor.java index bf7cb7cd99..41961bf4f3 100644 --- a/Core/src/org/sleuthkit/autopsy/core/AutopsyOptionProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/core/AutopsyOptionProcessor.java @@ -41,6 +41,7 @@ public class AutopsyOptionProcessor extends OptionProcessor { private static final Logger logger = Logger.getLogger(AutopsyOptionProcessor.class.getName()); private final Option liveAutopsyOption = Option.optionalArgument('l', "liveAutopsy"); + // @@@ We should centralize where we store this. It is defined in 2 other places. private final static String PROP_BASECASE = "LBL_BaseCase_PATH"; @@ -56,13 +57,20 @@ public class AutopsyOptionProcessor extends OptionProcessor { if(values.containsKey(liveAutopsyOption)){ try { RuntimeProperties.setRunningInTarget(true); - String[] dir= values.get(liveAutopsyOption); - String directory = dir == null ? PlatformUtil.getUserDirectory().toString() : dir[0]; - ModuleSettings.setConfigSetting(ModuleSettings.MAIN_SETTINGS, PROP_BASECASE, directory); + + // get the starting folder to store cases in + String[] argDirs= values.get(liveAutopsyOption); + String startingCaseDir; + if (argDirs == null || argDirs.length == 0) { + startingCaseDir = PlatformUtil.getUserDirectory().toString(); + } + else { + startingCaseDir = argDirs[0]; + } + ModuleSettings.setConfigSetting(ModuleSettings.MAIN_SETTINGS, PROP_BASECASE, startingCaseDir); } catch (RuntimeProperties.RuntimePropertiesException ex) { logger.log(Level.SEVERE, ex.getMessage(), ex); } } } - } diff --git a/Core/src/org/sleuthkit/autopsy/core/layer.xml b/Core/src/org/sleuthkit/autopsy/core/layer.xml index 342b58c4ee..8bdde0f317 100644 --- a/Core/src/org/sleuthkit/autopsy/core/layer.xml +++ b/Core/src/org/sleuthkit/autopsy/core/layer.xml @@ -77,7 +77,7 @@ - + @@ -198,7 +198,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java index 024686e26f..0752b67075 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java @@ -26,6 +26,7 @@ import java.awt.Graphics; import java.awt.dnd.DnDConstants; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.beans.FeatureDescriptor; import java.beans.PropertyVetoException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; @@ -37,8 +38,10 @@ import java.util.TreeMap; import java.util.TreeSet; import java.util.logging.Level; import java.util.prefs.Preferences; +import javax.swing.ImageIcon; import javax.swing.JTable; import javax.swing.ListSelectionModel; +import static javax.swing.SwingConstants.CENTER; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ListSelectionEvent; @@ -58,12 +61,14 @@ import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.nodes.Node.Property; +import org.openide.util.ImageUtilities; import org.openide.util.NbBundle; import org.openide.util.NbPreferences; import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.autopsy.datamodel.NodeSelectionInfo; /** @@ -82,6 +87,13 @@ public class DataResultViewerTable extends AbstractDataResultViewer { private static final long serialVersionUID = 1L; private static final Logger LOGGER = Logger.getLogger(DataResultViewerTable.class.getName()); + + private static final String NOTEPAD_ICON_PATH = "org/sleuthkit/autopsy/images/notepad16.png"; + private static final String RED_CIRCLE_ICON_PATH = "org/sleuthkit/autopsy/images/red-circle-exclamation.png"; + private static final String YELLOW_CIRCLE_ICON_PATH = "org/sleuthkit/autopsy/images/yellow-circle-yield.png"; + private static final ImageIcon COMMENT_ICON = new ImageIcon(ImageUtilities.loadImage(NOTEPAD_ICON_PATH, false)); + private static final ImageIcon INTERESTING_SCORE_ICON = new ImageIcon(ImageUtilities.loadImage(YELLOW_CIRCLE_ICON_PATH, false)); + private static final ImageIcon NOTABLE_ICON_SCORE = new ImageIcon(ImageUtilities.loadImage(RED_CIRCLE_ICON_PATH, false)); @NbBundle.Messages("DataResultViewerTable.firstColLbl=Name") static private final String FIRST_COLUMN_LABEL = Bundle.DataResultViewerTable_firstColLbl(); static private final Color TAGGED_ROW_COLOR = new Color(255, 255, 195); @@ -90,6 +102,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { private final Map> propertiesMap; private final Outline outline; private final TableListener outlineViewListener; + private final IconRendererTableListener iconRendererListener; private Node rootNode; /** @@ -102,7 +115,6 @@ public class DataResultViewerTable extends AbstractDataResultViewer { public DataResultViewerTable() { this(null, Bundle.DataResultViewerTable_title()); } - /** * Constructs a tabular result viewer that displays the children of a given @@ -157,6 +169,9 @@ public class DataResultViewerTable extends AbstractDataResultViewer { outlineViewListener = new TableListener(); outline.getColumnModel().addColumnModelListener(outlineViewListener); + iconRendererListener = new IconRendererTableListener(); + outline.getColumnModel().addColumnModelListener(iconRendererListener); + /* * Add a mouse listener to the child OutlineView (explorer view) to make * sure the first column of the table is kept in place. @@ -180,15 +195,15 @@ public class DataResultViewerTable extends AbstractDataResultViewer { /** * Gets the title of this tabular result viewer. - * @return + * + * @return title of tab. */ @Override @NbBundle.Messages("DataResultViewerTable.title=Table") public String getTitle() { return title; } - - + /** * Indicates whether a given node is supported as a root node for this * tabular viewer. @@ -210,11 +225,11 @@ public class DataResultViewerTable extends AbstractDataResultViewer { @Override @ThreadConfined(type = ThreadConfined.ThreadType.AWT) public void setNode(Node rootNode) { - if (! SwingUtilities.isEventDispatchThread()) { + if (!SwingUtilities.isEventDispatchThread()) { LOGGER.log(Level.SEVERE, "Attempting to run setNode() from non-EDT thread"); return; } - + /* * The quick filter must be reset because when determining column width, * ETable.getRowCount is called, and the documentation states that quick @@ -254,9 +269,9 @@ public class DataResultViewerTable extends AbstractDataResultViewer { } /** - * Sets up the Outline view of this tabular result viewer by creating - * column headers based on the children of the current root node. The - * persisted column order, sorting and visibility is used. + * Sets up the Outline view of this tabular result viewer by creating column + * headers based on the children of the current root node. The persisted + * column order, sorting and visibility is used. */ private void setupTable() { /* @@ -288,7 +303,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { * let the table resize itself. */ outline.setAutoResizeMode((props.isEmpty()) ? JTable.AUTO_RESIZE_ALL_COLUMNS : JTable.AUTO_RESIZE_OFF); - + assignColumns(props); // assign columns to match the properties if (firstProp != null) { ((DefaultOutlineModel) outline.getOutlineModel()).setNodesColumnLabel(firstProp.getDisplayName()); @@ -347,12 +362,13 @@ public class DataResultViewerTable extends AbstractDataResultViewer { * treated as un-hide/hide. */ outlineViewListener.listenToVisibilityChanges(true); + } /* - * Populates the column map for the child OutlineView of this tabular - * result viewer with references to the column objects for use when - * loading/storing the visibility info. + * Populates the column map for the child OutlineView of this tabular result + * viewer with references to the column objects for use when loading/storing + * the visibility info. */ private void populateColumnMap() { columnMap.clear(); @@ -364,6 +380,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { if (entry.getKey() < columnCount) { final ETableColumn column = (ETableColumn) columnModel.getColumn(entry.getKey()); columnMap.put(propName, column); + } } } @@ -408,8 +425,8 @@ public class DataResultViewerTable extends AbstractDataResultViewer { outline.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); } } - - protected TableColumnModel getColumnModel(){ + + protected TableColumnModel getColumnModel() { return outline.getColumnModel(); } @@ -640,6 +657,57 @@ public class DataResultViewerTable extends AbstractDataResultViewer { } } + /** + * Listener which sets the custom icon renderer on columns which contain + * icons instead of text when a column is added. + */ + private class IconRendererTableListener implements TableColumnModelListener { + + @NbBundle.Messages({"DataResultViewerTable.commentRender.name=C", + "DataResultViewerTable.scoreRender.name=S", + "DataResultViewerTable.countRender.name=O"}) + @Override + public void columnAdded(TableColumnModelEvent e) { + if (e.getSource() instanceof ETableColumnModel) { + TableColumn column = ((TableColumnModel) e.getSource()).getColumn(e.getToIndex()); + if (column.getHeaderValue().toString().equals(Bundle.DataResultViewerTable_commentRender_name())) { + //if the current column is a comment column set the cell renderer to be the HasCommentCellRenderer + column.setCellRenderer(new HasCommentCellRenderer()); + } else if (column.getHeaderValue().toString().equals(Bundle.DataResultViewerTable_scoreRender_name())) { + //if the current column is a score column set the cell renderer to be the ScoreCellRenderer + column.setCellRenderer(new ScoreCellRenderer()); + } else if (column.getHeaderValue().toString().equals(Bundle.DataResultViewerTable_countRender_name())) { + column.setCellRenderer(new CountCellRenderer()); + } + } + } + + @Override + public void columnRemoved(TableColumnModelEvent e + ) { + //Don't do anything when column removed + } + + @Override + public void columnMoved(TableColumnModelEvent e + ) { + //Don't do anything when column moved + } + + @Override + public void columnMarginChanged(ChangeEvent e + ) { + //Don't do anything when column margin changed + } + + @Override + public void columnSelectionChanged(ListSelectionEvent e + ) { + //Don't do anything when column selection changed + } + + } + /** * Listens to mouse events and table column events and persists column order * sorting, and visibility changes. @@ -801,6 +869,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { try { tagFound = !prop.getValue().equals(""); } catch (IllegalAccessException | InvocationTargetException ignore) { + //if unable to get the tags property value, treat it like it not having a comment } break; } @@ -816,6 +885,174 @@ public class DataResultViewerTable extends AbstractDataResultViewer { } } + /* + * A renderer which based on the contents of the cell will display an icon + * to indicate the presence of a comment related to the content. + */ + private final class HasCommentCellRenderer extends ColorTagCustomRenderer { + + private static final long serialVersionUID = 1L; + + @NbBundle.Messages({"DataResultViewerTable.commentRenderer.crComment.toolTip=Comment exists in Central Repository", + "DataResultViewerTable.commentRenderer.tagComment.toolTip=Comment exists on associated tag(s)", + "DataResultViewerTable.commentRenderer.crAndTagComment.toolTip=Comments exist both in Central Repository and on associated tag(s)", + "DataResultViewerTable.commentRenderer.noComment.toolTip=No comments found"}) + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + setBackground(component.getBackground()); //inherit highlighting + setHorizontalAlignment(CENTER); + Object switchValue = null; + if ((value instanceof NodeProperty)) { + //The Outline view has properties in the cell, the value contained in the property is what we want + try { + switchValue = ((Node.Property) value).getValue(); + } catch (IllegalAccessException | InvocationTargetException ex) { + //Unable to get the value from the NodeProperty no Icon will be displayed + } + } else { + //JTables contain the value we want directly in the cell + switchValue = value; + } + setText(""); + if ((switchValue instanceof HasCommentStatus)) { + + switch ((HasCommentStatus) switchValue) { + case CR_COMMENT: + setIcon(COMMENT_ICON); + setToolTipText(Bundle.DataResultViewerTable_commentRenderer_crComment_toolTip()); + break; + case TAG_COMMENT: + setIcon(COMMENT_ICON); + setToolTipText(Bundle.DataResultViewerTable_commentRenderer_tagComment_toolTip()); + break; + case CR_AND_TAG_COMMENTS: + setIcon(COMMENT_ICON); + setToolTipText(Bundle.DataResultViewerTable_commentRenderer_crAndTagComment_toolTip()); + break; + case TAG_NO_COMMENT: + case NO_COMMENT: + default: + setIcon(null); + setToolTipText(Bundle.DataResultViewerTable_commentRenderer_noComment_toolTip()); + } + } else { + setIcon(null); + } + + return this; + } + + } + + /* + * A renderer which based on the contents of the cell will display an icon + * to indicate the score associated with the item. + */ + private final class ScoreCellRenderer extends ColorTagCustomRenderer { + + private static final long serialVersionUID = 1L; + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + setBackground(component.getBackground()); //inherit highlighting + setHorizontalAlignment(CENTER); + Object switchValue = null; + if ((value instanceof NodeProperty)) { + //The Outline view has properties in the cell, the value contained in the property is what we want + try { + switchValue = ((Node.Property) value).getValue(); + setToolTipText(((FeatureDescriptor) value).getShortDescription()); + } catch (IllegalAccessException | InvocationTargetException ex) { + //Unable to get the value from the NodeProperty no Icon will be displayed + } + + } else { + //JTables contain the value we want directly in the cell + switchValue = value; + } + setText(""); + if ((switchValue instanceof Score)) { + + switch ((Score) switchValue) { + case INTERESTING_SCORE: + setIcon(INTERESTING_SCORE_ICON); + break; + case NOTABLE_SCORE: + setIcon(NOTABLE_ICON_SCORE); + break; + case NO_SCORE: + default: + setIcon(null); + } + } else { + setIcon(null); + } + return this; + } + + } + + /* + * A renderer which based on the contents of the cell will display an empty + * cell if no count was available. + */ + private final class CountCellRenderer extends ColorTagCustomRenderer { + + private static final long serialVersionUID = 1L; + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + setBackground(component.getBackground()); //inherit highlighting + setHorizontalAlignment(LEFT); + Object countValue = null; + if ((value instanceof NodeProperty)) { + //The Outline view has properties in the cell, the value contained in the property is what we want + try { + countValue = ((Node.Property) value).getValue(); + setToolTipText(((FeatureDescriptor) value).getShortDescription()); + } catch (IllegalAccessException | InvocationTargetException ex) { + //Unable to get the value from the NodeProperty no Icon will be displayed + } + } else { + //JTables contain the value we want directly in the cell + countValue = value; + } + setText(""); + if ((countValue instanceof Long)) { + //Don't display value if value is negative used so that sorting will behave as desired + if ((Long) countValue >= 0) { + setText(countValue.toString()); + } + } + return this; + } + + } + + /** + * Enum to denote the presence of a comment associated with the content or + * artifacts generated from it. + */ + public enum HasCommentStatus { + NO_COMMENT, + TAG_NO_COMMENT, + CR_COMMENT, + TAG_COMMENT, + CR_AND_TAG_COMMENTS + } + + /** + * Enum to denote the score given to an item to draw the users attention + */ + public enum Score { + NO_SCORE, + INTERESTING_SCORE, + NOTABLE_SCORE + } + /** * 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 diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java index 0f7fb6b576..5a3b75ca69 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java @@ -107,7 +107,7 @@ public class ImageUtils { * NOTE: Must be cleared when the case is changed. */ @Messages({"ImageUtils.ffmpegLoadedError.title=OpenCV FFMpeg", - "ImageUtils.ffmpegLoadedError.msg=OpenCV FFMpeg library failed to load, see log for more details"}) + "ImageUtils.ffmpegLoadedError.msg=OpenCV FFMpeg library failed to load, see log for more details"}) private static final ConcurrentHashMap cacheFileMap = new ConcurrentHashMap<>(); static { @@ -218,7 +218,7 @@ public class ImageUtils { } return VideoUtils.isVideoThumbnailSupported(file) - || isImageThumbnailSupported(file); + || isImageThumbnailSupported(file); } /** @@ -413,7 +413,7 @@ public class ImageUtils { String cacheDirectory = Case.getCurrentCaseThrows().getCacheDirectory(); return Paths.get(cacheDirectory, "thumbnails", fileID + ".png").toFile(); //NON-NLS } catch (NoCurrentCaseException e) { - LOGGER.log(Level.WARNING, "Could not get cached thumbnail location. No case is open."); //NON-NLS + LOGGER.log(Level.INFO, "Could not get cached thumbnail location. No case is open."); //NON-NLS return null; } }); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java index b4f8f76728..204446ba24 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java @@ -34,17 +34,28 @@ import org.openide.util.NbBundle; import org.openide.util.WeakListeners; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.casemodule.events.CommentChangedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamArtifactUtil; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbUtil; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable.Score; import org.sleuthkit.autopsy.coreutils.Logger; import static org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode.AbstractFilePropertyType.*; import static org.sleuthkit.autopsy.datamodel.Bundle.*; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable.HasCommentStatus; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.ModuleContentEvent; import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; /** * An abstract node that encapsulates AbstractFile data @@ -58,7 +69,7 @@ public abstract class AbstractAbstractFileNode extends A private static final String NO_DESCR = AbstractAbstractFileNode_addFileProperty_desc(); private static final Set CASE_EVENTS_OF_INTEREST = EnumSet.of(Case.Events.CURRENT_CASE, - Case.Events.CONTENT_TAG_ADDED, Case.Events.CONTENT_TAG_DELETED); + Case.Events.CONTENT_TAG_ADDED, Case.Events.CONTENT_TAG_DELETED, Case.Events.CR_COMMENT_CHANGED); /** * @param abstractFile file to wrap @@ -145,6 +156,11 @@ public abstract class AbstractAbstractFileNode extends A if (event.getDeletedTagInfo().getContentID() == content.getId()) { updateSheet(); } + } else if (eventType.equals(Case.Events.CR_COMMENT_CHANGED.toString())) { + CommentChangedEvent event = (CommentChangedEvent) evt; + if (event.getContentID() == content.getId()) { + updateSheet(); + } } }; @@ -253,6 +269,133 @@ public abstract class AbstractAbstractFileNode extends A map.put(EXTENSION.toString(), content.getNameExtension()); } + /** + * Get all tags from the case database that are associated with the file + * + * @return a list of tags that are associated with the file + */ + protected final List getContentTagsFromDatabase() { + List tags = new ArrayList<>(); + try { + tags.addAll(Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsByContent(content)); + } catch (TskCoreException | NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Failed to get tags for content " + content.getName(), ex); + } + return tags; + } + + protected final CorrelationAttributeInstance getCorrelationAttributeInstance() { + CorrelationAttributeInstance correlationAttribute = null; + if (EamDbUtil.useCentralRepo()) { + correlationAttribute = EamArtifactUtil.getInstanceFromContent(content); + } + return correlationAttribute; + } + + /** + * Used by subclasses of AbstractAbstractFileNode to add the comment + * property to their sheets. + * + * @param sheetSet the modifiable Sheet.Set returned by + * Sheet.get(Sheet.PROPERTIES) + * @param tags the list of tags associated with the file + * @param attribute the correlation attribute associated with this file, + * null if central repo is not enabled + */ + @NbBundle.Messages({"AbstractAbstractFileNode.createSheet.comment.name=C", + "AbstractAbstractFileNode.createSheet.comment.displayName=C"}) + protected final void addCommentProperty(Sheet.Set sheetSet, List tags, CorrelationAttributeInstance attribute) { + + HasCommentStatus status = tags.size() > 0 ? HasCommentStatus.TAG_NO_COMMENT : HasCommentStatus.NO_COMMENT; + + for (ContentTag tag : tags) { + if (!StringUtils.isBlank(tag.getComment())) { + //if the tag is null or empty or contains just white space it will indicate there is not a comment + status = HasCommentStatus.TAG_COMMENT; + break; + } + } + if (attribute != null && !StringUtils.isBlank(attribute.getComment())) { + if (status == HasCommentStatus.TAG_COMMENT) { + status = HasCommentStatus.CR_AND_TAG_COMMENTS; + } else { + status = HasCommentStatus.CR_COMMENT; + } + } + sheetSet.put(new NodeProperty<>(AbstractAbstractFileNode_createSheet_comment_name(), AbstractAbstractFileNode_createSheet_comment_displayName(), NO_DESCR, + status)); + } + + /** + * Used by subclasses of AbstractAbstractFileNode to add the Score property + * to their sheets. + * + * @param sheetSet the modifiable Sheet.Set returned by + * Sheet.get(Sheet.PROPERTIES) + * @param tags the list of tags associated with the file + */ + @NbBundle.Messages({"AbstractAbstractFileNode.createSheet.score.name=S", + "AbstractAbstractFileNode.createSheet.score.displayName=S", + "AbstractAbstractFileNode.createSheet.notableFile.description=File recognized as notable.", + "AbstractAbstractFileNode.createSheet.interestingResult.description=File has interesting result associated with it.", + "AbstractAbstractFileNode.createSheet.taggedFile.description=File has been tagged.", + "AbstractAbstractFileNode.createSheet.notableTaggedFile.description=File tagged with notable tag."}) + protected final void addScoreProperty(Sheet.Set sheetSet, List tags) { + Score score = Score.NO_SCORE; + String description = NO_DESCR; + if (content.getKnown() == TskData.FileKnown.BAD) { + score = Score.NOTABLE_SCORE; + description = Bundle.AbstractAbstractFileNode_createSheet_notableFile_description(); + } + try { + if (score == Score.NO_SCORE && !content.getArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT).isEmpty()) { + score = Score.INTERESTING_SCORE; + description = Bundle.AbstractAbstractFileNode_createSheet_interestingResult_description(); + } + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Error getting artifacts for file: " + content.getName(), ex); + } + if (tags.size() > 0 && (score == Score.NO_SCORE || score == Score.INTERESTING_SCORE)) { + score = Score.INTERESTING_SCORE; + description = Bundle.AbstractAbstractFileNode_createSheet_taggedFile_description(); + for (ContentTag tag : tags) { + if (tag.getName().getKnownStatus() == TskData.FileKnown.BAD) { + score = Score.NOTABLE_SCORE; + description = Bundle.AbstractAbstractFileNode_createSheet_notableTaggedFile_description(); + break; + } + } + } + sheetSet.put(new NodeProperty<>(Bundle.AbstractAbstractFileNode_createSheet_score_name(), Bundle.AbstractAbstractFileNode_createSheet_score_displayName(), description, score)); + } + + @NbBundle.Messages({"AbstractAbstractFileNode.createSheet.count.name=O", + "AbstractAbstractFileNode.createSheet.count.displayName=O", + "AbstractAbstractFileNode.createSheet.count.noCentralRepo.description=Central repository was not enabled when this column was populated", + "AbstractAbstractFileNode.createSheet.count.hashLookupNotRun.description=Hash lookup had not been run on this file when the column was populated", + "# {0} - occuranceCount", + "AbstractAbstractFileNode.createSheet.count.description=There were {0} datasource(s) found with occurances of the correlation value"}) + protected final void addCountProperty(Sheet.Set sheetSet, CorrelationAttributeInstance attribute) { + Long count = -1L; //The column renderer will not display negative values, negative value used when count unavailble to preserve sorting + String description = Bundle.AbstractAbstractFileNode_createSheet_count_noCentralRepo_description(); + try { + //don't perform the query if there is no correlation value + if (attribute != null && StringUtils.isNotBlank(attribute.getCorrelationValue())) { + count = EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(attribute.getCorrelationType(), attribute.getCorrelationValue()); + description = Bundle.AbstractAbstractFileNode_createSheet_count_description(count); + } else if (attribute != null) { + description = Bundle.AbstractAbstractFileNode_createSheet_count_hashLookupNotRun_description(); + } + } catch (EamDbException ex) { + logger.log(Level.WARNING, "Error getting count of datasources with correlation attribute", ex); + } catch (CorrelationAttributeNormalizationException ex) { + logger.log(Level.WARNING, "Unable to normalize data to get count of datasources with correlation attribute", ex); + } + + sheetSet.put( + new NodeProperty<>(Bundle.AbstractAbstractFileNode_createSheet_count_name(), Bundle.AbstractAbstractFileNode_createSheet_count_displayName(), description, count)); + } + /** * Used by subclasses of AbstractAbstractFileNode to add the tags property * to their sheets. @@ -261,6 +404,7 @@ public abstract class AbstractAbstractFileNode extends A * Sheet.get(Sheet.PROPERTIES) */ @NbBundle.Messages("AbstractAbstractFileNode.tagsProperty.displayName=Tags") + @Deprecated protected void addTagProperty(Sheet.Set sheetSet) { List tags = new ArrayList<>(); try { @@ -274,6 +418,21 @@ public abstract class AbstractAbstractFileNode extends A .collect(Collectors.joining(", ")))); } + /** + * Used by subclasses of AbstractAbstractFileNode to add the tags property + * to their sheets. + * + * @param sheetSet the modifiable Sheet.Set returned by + * Sheet.get(Sheet.PROPERTIES) + * @param tags the list of tags associated with the file + */ + protected final void addTagProperty(Sheet.Set sheetSet, List tags) { + sheetSet.put(new NodeProperty<>("Tags", AbstractAbstractFileNode_tagsProperty_displayName(), + NO_DESCR, tags.stream().map(t -> t.getName().getDisplayName()) + .distinct() + .collect(Collectors.joining(", ")))); + } + private static String getContentPath(AbstractFile file) { try { return file.getUniquePath(); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractFsContentNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractFsContentNode.java index 824407c417..02dd3c1d28 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractFsContentNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractFsContentNode.java @@ -19,11 +19,14 @@ package org.sleuthkit.autopsy.datamodel; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.ContentTag; /** * Abstract class that implements the commonality between File and Directory @@ -70,11 +73,21 @@ public abstract class AbstractFsContentNode extends Abst sheetSet = Sheet.createPropertiesSet(); sheet.put(sheetSet); } - + List tags = getContentTagsFromDatabase(); Map map = new LinkedHashMap<>(); fillPropertyMap(map, getContent()); - final String NO_DESCR = Bundle.AbstractFsContentNode_noDesc_text(); + //add the name property before the comment property to ensure it is first column + sheetSet.put(new NodeProperty<>(AbstractFilePropertyType.NAME.toString(), + AbstractFilePropertyType.NAME.toString(), + NO_DESCR, + getName())); + //add the cr status property before the propertyMap to ensure it is early in column order + addScoreProperty(sheetSet, tags); + //add the comment property before the propertyMap to ensure it is early in column order + CorrelationAttributeInstance correlationAttribute = getCorrelationAttributeInstance(); + addCommentProperty(sheetSet, tags, correlationAttribute); + addCountProperty(sheetSet, correlationAttribute); for (AbstractFilePropertyType propType : AbstractFilePropertyType.values()) { final String propString = propType.toString(); sheetSet.put(new NodeProperty<>(propString, propString, NO_DESCR, map.get(propString))); @@ -84,7 +97,7 @@ public abstract class AbstractFsContentNode extends Abst } // add tags property to the sheet - addTagProperty(sheetSet); + addTagProperty(sheetSet, tags); return sheet; } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java index 5b20c92c6e..4178d2fafe 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java @@ -46,11 +46,20 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagDeletedEvent; +import org.sleuthkit.autopsy.casemodule.events.CommentChangedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamArtifactUtil; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbUtil; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable.Score; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import static org.sleuthkit.autopsy.datamodel.DisplayableItemNode.findLinked; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable.HasCommentStatus; import org.sleuthkit.autopsy.timeline.actions.ViewArtifactInTimelineAction; import org.sleuthkit.autopsy.timeline.actions.ViewFileInTimelineAction; import org.sleuthkit.datamodel.AbstractFile; @@ -61,6 +70,7 @@ import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; /** * Node wrapping a blackboard artifact object. This is generated from several @@ -73,7 +83,8 @@ public class BlackboardArtifactNode extends AbstractContentNode contentCache = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.MINUTES). @@ -127,6 +138,11 @@ public class BlackboardArtifactNode extends AbstractContentNode tags = getAllTagsFromDatabase(); + Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES); if (sheetSet == null) { sheetSet = Sheet.createPropertiesSet(); @@ -332,6 +350,10 @@ public class BlackboardArtifactNode extends AbstractContentNode getAllTagsFromDatabase() { + List tags = new ArrayList<>(); + try { + tags.addAll(Case.getCurrentCaseThrows().getServices().getTagsManager().getBlackboardArtifactTagsByArtifact(artifact)); + tags.addAll(Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsByContent(associated)); + } catch (TskCoreException | NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Failed to get tags for artifact " + artifact.getDisplayName(), ex); + } + return tags; + } + /** * Used by (subclasses of) BlackboardArtifactNode to add the tags property * to their sheets. @@ -494,6 +533,7 @@ public class BlackboardArtifactNode extends AbstractContentNode tags = new ArrayList<>(); @@ -507,6 +547,137 @@ public class BlackboardArtifactNode extends AbstractContentNode t.getName().getDisplayName()).collect(Collectors.joining(", ")))); } + /** + * Used by (subclasses of) BlackboardArtifactNode to add the tags property + * to their sheets. + * + * @param sheetSet the modifiable Sheet.Set returned by + * Sheet.get(Sheet.PROPERTIES) + * @param tags the list of tags which should appear as the value for the + * property + */ + protected final void addTagProperty(Sheet.Set sheetSet, List tags) { + sheetSet.put(new NodeProperty<>("Tags", Bundle.BlackboardArtifactNode_createSheet_tags_displayName(), + NO_DESCR, tags.stream().map(t -> t.getName().getDisplayName()).collect(Collectors.joining(", ")))); + } + + protected final CorrelationAttributeInstance getCorrelationAttributeInstance() { + CorrelationAttributeInstance correlationAttribute = null; + if (EamDbUtil.useCentralRepo()) { + correlationAttribute = EamArtifactUtil.getInstanceFromContent(associated); + } + return correlationAttribute; + } + + /** + * Used by (subclasses of) BlackboardArtifactNode to add the comment + * property to their sheets. + * + * @param sheetSet the modifiable Sheet.Set returned by + * Sheet.get(Sheet.PROPERTIES) + * @param tags the list of tags associated with the file + * @param attribute the correlation attribute associated with this + * artifact's associated file, null if central repo is not + * enabled + */ + @NbBundle.Messages({"BlackboardArtifactNode.createSheet.comment.name=C", + "BlackboardArtifactNode.createSheet.comment.displayName=C"}) + protected final void addCommentProperty(Sheet.Set sheetSet, List tags, CorrelationAttributeInstance attribute) { + HasCommentStatus status = tags.size() > 0 ? HasCommentStatus.TAG_NO_COMMENT : HasCommentStatus.NO_COMMENT; + for (Tag tag : tags) { + if (!StringUtils.isBlank(tag.getComment())) { + //if the tag is null or empty or contains just white space it will indicate there is not a comment + status = HasCommentStatus.TAG_COMMENT; + break; + } + } + //currently checks for a comment on the associated file in the central repo not the artifact itself + //what we want the column property to reflect should be revisted when we have added a way to comment + //on the artifact itself + if (attribute != null && !StringUtils.isBlank(attribute.getComment())) { + if (status == HasCommentStatus.TAG_COMMENT) { + status = HasCommentStatus.CR_AND_TAG_COMMENTS; + } else { + status = HasCommentStatus.CR_COMMENT; + } + } + sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), NO_DESCR, + status)); + } + + /** + * Used by (subclasses of) BlackboardArtifactNode to add the Score property + * to their sheets. + * + * @param sheetSet the modifiable Sheet.Set returned by + * Sheet.get(Sheet.PROPERTIES) + * @param tags the list of tags associated with the file + */ + @NbBundle.Messages({"BlackboardArtifactNode.createSheet.score.name=S", + "BlackboardArtifactNode.createSheet.score.displayName=S", + "BlackboardArtifactNode.createSheet.notableFile.description=Associated file recognized as notable.", + "BlackboardArtifactNode.createSheet.interestingResult.description=Result has an interesting result associated with it.", + "BlackboardArtifactNode.createSheet.taggedItem.description=Result or associated file has been tagged.", + "BlackboardArtifactNode.createSheet.notableTaggedItem.description=Result or associated file tagged with notable tag."}) + protected final void addScoreProperty(Sheet.Set sheetSet, List tags) { + Score score = Score.NO_SCORE; + String description = ""; + if (associated instanceof AbstractFile) { + if (((AbstractFile) associated).getKnown() == TskData.FileKnown.BAD) { + score = Score.NOTABLE_SCORE; + description = Bundle.BlackboardArtifactNode_createSheet_notableFile_description(); + } + } + try { + if (score == Score.NO_SCORE && !content.getArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT).isEmpty()) { + score = Score.INTERESTING_SCORE; + description = Bundle.BlackboardArtifactNode_createSheet_interestingResult_description(); + } + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Error getting artifacts for artifact: " + content.getName(), ex); + } + if (tags.size() > 0 && (score == Score.NO_SCORE || score == Score.INTERESTING_SCORE)) { + score = Score.INTERESTING_SCORE; + description = Bundle.BlackboardArtifactNode_createSheet_taggedItem_description(); + for (Tag tag : tags) { + if (tag.getName().getKnownStatus() == TskData.FileKnown.BAD) { + score = Score.NOTABLE_SCORE; + description = Bundle.BlackboardArtifactNode_createSheet_notableTaggedItem_description(); + break; + } + } + } + sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), description, score)); + } + + @NbBundle.Messages({"BlackboardArtifactNode.createSheet.count.name=O", + "BlackboardArtifactNode.createSheet.count.displayName=O", + "BlackboardArtifactNode.createSheet.count.noCentralRepo.description=Central repository was not enabled when this column was populated", + "BlackboardArtifactNode.createSheet.count.hashLookupNotRun.description=Hash lookup had not been run on this artifact's associated file when the column was populated", + "# {0} - occuranceCount", + "BlackboardArtifactNode.createSheet.count.description=There were {0} datasource(s) found with occurances of the correlation value"}) + + protected final void addCountProperty(Sheet.Set sheetSet, CorrelationAttributeInstance attribute) { + Long count = -1L; //The column renderer will not display negative values, negative value used when count unavailble to preserve sorting + String description = Bundle.BlackboardArtifactNode_createSheet_count_noCentralRepo_description(); + try { + //don't perform the query if there is no correlation value + if (attribute != null && StringUtils.isNotBlank(attribute.getCorrelationValue())) { + count = EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(attribute.getCorrelationType(), attribute.getCorrelationValue()); + description = Bundle.BlackboardArtifactNode_createSheet_count_description(count); + } else if (attribute != null) { + description = Bundle.BlackboardArtifactNode_createSheet_count_hashLookupNotRun_description(); + } + } catch (EamDbException ex) { + logger.log(Level.WARNING, "Error getting count of datasources with correlation attribute", ex); + } + catch (CorrelationAttributeNormalizationException ex) { + logger.log(Level.WARNING, "Unable to normalize data to get count of datasources with correlation attribute", ex); + } + sheetSet.put( + new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), description, count)); + } + private void updateSheet() { this.setSheet(createSheet()); } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java index c7f20b5c28..247e7cd76e 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java @@ -31,11 +31,13 @@ import org.openide.util.NbBundle; import org.openide.util.Utilities; import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.LayoutFile; import org.sleuthkit.datamodel.TskData; @@ -79,6 +81,8 @@ public class LayoutFileNode extends AbstractAbstractFileNode { sheet.put(sheetSet); } + List tags = getContentTagsFromDatabase(); + Map map = new LinkedHashMap<>(); fillPropertyMap(map); @@ -86,14 +90,17 @@ public class LayoutFileNode extends AbstractAbstractFileNode { NbBundle.getMessage(this.getClass(), "LayoutFileNode.createSheet.name.displayName"), NbBundle.getMessage(this.getClass(), "LayoutFileNode.createSheet.name.desc"), getName())); - + CorrelationAttributeInstance correlationAttribute = getCorrelationAttributeInstance(); + addScoreProperty(sheetSet, tags); + addCommentProperty(sheetSet, tags, correlationAttribute); + addCountProperty(sheetSet, correlationAttribute); final String NO_DESCR = NbBundle.getMessage(this.getClass(), "LayoutFileNode.createSheet.noDescr.text"); for (Map.Entry entry : map.entrySet()) { sheetSet.put(new NodeProperty<>(entry.getKey(), entry.getKey(), NO_DESCR, entry.getValue())); } // add tags property to the sheet - addTagProperty(sheetSet); + addTagProperty(sheetSet, tags); return sheet; } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/LocalDirectoryNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/LocalDirectoryNode.java index 9b360763a5..f6f3de7680 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/LocalDirectoryNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/LocalDirectoryNode.java @@ -19,9 +19,12 @@ package org.sleuthkit.autopsy.datamodel; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.LocalDirectory; /** @@ -55,11 +58,15 @@ public class LocalDirectoryNode extends SpecialDirectoryNode { sheet.put(sheetSet); } + List tags = getContentTagsFromDatabase(); sheetSet.put(new NodeProperty<>(Bundle.LocalDirectoryNode_createSheet_name_name(), Bundle.LocalDirectoryNode_createSheet_name_displayName(), Bundle.LocalDirectoryNode_createSheet_name_desc(), getName())); - + CorrelationAttributeInstance correlationAttribute = getCorrelationAttributeInstance(); + addScoreProperty(sheetSet, tags); + addCommentProperty(sheetSet, tags, correlationAttribute); + addCountProperty(sheetSet, correlationAttribute); // At present, a LocalDirectory will never be a datasource - the top level of a logical // file set is a VirtualDirectory Map map = new LinkedHashMap<>(); @@ -69,7 +76,7 @@ public class LocalDirectoryNode extends SpecialDirectoryNode { for (Map.Entry entry : map.entrySet()) { sheetSet.put(new NodeProperty<>(entry.getKey(), entry.getKey(), NO_DESCR, entry.getValue())); } - addTagProperty(sheetSet); + addTagProperty(sheetSet, tags); return sheet; } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java index 00c63b745e..a9c09ee1d7 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java @@ -32,6 +32,7 @@ import org.openide.util.NbBundle; import org.openide.util.Utilities; import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; @@ -41,6 +42,7 @@ import org.sleuthkit.autopsy.directorytree.ViewContextAction; import org.sleuthkit.autopsy.modules.embeddedfileextractor.ExtractArchiveWithPasswordAction; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.TskCoreException; /** @@ -72,7 +74,7 @@ public class LocalFileNode extends AbstractAbstractFileNode { sheetSet = Sheet.createPropertiesSet(); sheet.put(sheetSet); } - + List tags = getContentTagsFromDatabase(); Map map = new LinkedHashMap<>(); fillPropertyMap(map, getContent()); @@ -80,14 +82,17 @@ public class LocalFileNode extends AbstractAbstractFileNode { NbBundle.getMessage(this.getClass(), "LocalFileNode.createSheet.name.displayName"), NbBundle.getMessage(this.getClass(), "LocalFileNode.createSheet.name.desc"), getName())); - + CorrelationAttributeInstance correlationAttribute = getCorrelationAttributeInstance(); + addScoreProperty(sheetSet, tags); + addCommentProperty(sheetSet, tags, correlationAttribute); + addCountProperty(sheetSet, correlationAttribute); final String NO_DESCR = NbBundle.getMessage(this.getClass(), "LocalFileNode.createSheet.noDescr.text"); for (Map.Entry entry : map.entrySet()) { sheetSet.put(new NodeProperty<>(entry.getKey(), entry.getKey(), NO_DESCR, entry.getValue())); } // add tags property to the sheet - addTagProperty(sheetSet); + addTagProperty(sheetSet, tags); return sheet; } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/VirtualDirectoryNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/VirtualDirectoryNode.java index 837a2ae268..02f382e6ad 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/VirtualDirectoryNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/VirtualDirectoryNode.java @@ -21,13 +21,16 @@ package org.sleuthkit.autopsy.datamodel; import java.sql.ResultSet; import java.sql.SQLException; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.logging.Level; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.VirtualDirectory; @@ -79,14 +82,18 @@ public class VirtualDirectoryNode extends SpecialDirectoryNode { sheetSet = Sheet.createPropertiesSet(); sheet.put(sheetSet); } - + List tags = getContentTagsFromDatabase(); + sheetSet.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "VirtualDirectoryNode.createSheet.name.name"), NbBundle.getMessage(this.getClass(), "VirtualDirectoryNode.createSheet.name.displayName"), NbBundle.getMessage(this.getClass(), "VirtualDirectoryNode.createSheet.name.desc"), getName())); - if (!this.content.isDataSource()) { + CorrelationAttributeInstance correlationAttribute = getCorrelationAttributeInstance(); + addScoreProperty(sheetSet, tags); + addCommentProperty(sheetSet, tags, correlationAttribute); + addCountProperty(sheetSet, correlationAttribute); Map map = new LinkedHashMap<>(); fillPropertyMap(map, getContent()); @@ -94,7 +101,7 @@ public class VirtualDirectoryNode extends SpecialDirectoryNode { for (Map.Entry entry : map.entrySet()) { sheetSet.put(new NodeProperty<>(entry.getKey(), entry.getKey(), NO_DESCR, entry.getValue())); } - addTagProperty(sheetSet); + addTagProperty(sheetSet, tags); } else { sheetSet.put(new NodeProperty<>(Bundle.VirtualDirectoryNode_createSheet_type_name(), Bundle.VirtualDirectoryNode_createSheet_type_displayName(), diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/SelectionContext.java b/Core/src/org/sleuthkit/autopsy/directorytree/SelectionContext.java index c204be3b00..de84a49e85 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/SelectionContext.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/SelectionContext.java @@ -23,11 +23,13 @@ import org.openide.util.NbBundle; import static org.sleuthkit.autopsy.directorytree.Bundle.*; @NbBundle.Messages({"SelectionContext.dataSources=Data Sources", + "SelectionContext.dataSourceFiles=Data Source Files", "SelectionContext.views=Views"}) enum SelectionContext { DATA_SOURCES(SelectionContext_dataSources()), VIEWS(SelectionContext_views()), - OTHER(""); // Subnode of another node. + OTHER(""), // Subnode of another node. + DATA_SOURCE_FILES(SelectionContext_dataSourceFiles()); private final String displayName; @@ -36,7 +38,7 @@ enum SelectionContext { } public static SelectionContext getContextFromName(String name) { - if (name.equals(DATA_SOURCES.getName())) { + if (name.equals(DATA_SOURCES.getName()) || name.equals(DATA_SOURCE_FILES.getName())) { return DATA_SOURCES; } else if (name.equals(VIEWS.getName())) { return VIEWS; @@ -64,6 +66,16 @@ enum SelectionContext { // One level below root node. Should be one of DataSources, Views, or Results return SelectionContext.getContextFromName(n.getDisplayName()); } else { + // In Group by Data Source mode, the node under root is the data source name, and + // under that is Data Source Files, Views, or Results. Before moving up the tree, check + // if one of those applies. + if (n.getParentNode().getParentNode().getParentNode() == null) { + SelectionContext context = SelectionContext.getContextFromName(n.getDisplayName()); + if (context != SelectionContext.OTHER) { + return context; + } + } + return getSelectionContext(n.getParentNode()); } } diff --git a/Core/src/org/sleuthkit/autopsy/images/notepad16.png b/Core/src/org/sleuthkit/autopsy/images/notepad16.png new file mode 100644 index 0000000000..9f6db6d7a5 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/notepad16.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/red-circle-exclamation.png b/Core/src/org/sleuthkit/autopsy/images/red-circle-exclamation.png new file mode 100644 index 0000000000..26b61e6f06 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/red-circle-exclamation.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/yellow-circle-yield.png b/Core/src/org/sleuthkit/autopsy/images/yellow-circle-yield.png new file mode 100644 index 0000000000..85c873f33f Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/yellow-circle-yield.png differ diff --git a/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisCompletedEvent.java b/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisCompletedEvent.java index 0df66fabb6..4626b6858a 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisCompletedEvent.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisCompletedEvent.java @@ -26,7 +26,7 @@ import org.sleuthkit.datamodel.Content; * Event published when analysis (ingest) of a data source included in an ingest * job is completed. */ -public class DataSourceAnalysisCompletedEvent extends DataSourceAnalysisEvent implements Serializable { +public final class DataSourceAnalysisCompletedEvent extends DataSourceAnalysisEvent implements Serializable { /** * The reason why the analysis of the data source completed. diff --git a/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisStartedEvent.java b/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisStartedEvent.java index 6975120eae..b64a224da0 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisStartedEvent.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisStartedEvent.java @@ -26,7 +26,7 @@ import org.sleuthkit.datamodel.Content; * Event published when analysis (ingest) of a data source included in an ingest * job is started. */ -public class DataSourceAnalysisStartedEvent extends DataSourceAnalysisEvent implements Serializable { +public final class DataSourceAnalysisStartedEvent extends DataSourceAnalysisEvent implements Serializable { private static final long serialVersionUID = 1L; diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/Bundle.properties b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/Bundle.properties index 5b8d772f9d..e81e5d2fc0 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/Bundle.properties @@ -86,6 +86,7 @@ HashDbImportDatabaseDialog.failedToGetDbPathMsg=Failed to get the path of the se HashDbImportDatabaseDialog.importHashDbErr=Import Hash Set Error HashDbImportDatabaseDialog.mustSelectHashDbFilePathMsg=A hash set file path must be selected. HashDbImportDatabaseDialog.hashDbDoesNotExistMsg=The selected hash set does not exist. +HashDbImportDatabaseDialog.unableToCopyToUserDirMsg=Unable to copy the hash set to user configuration directory {0}. HashDbImportDatabaseDialog.errorMessage.failedToOpenHashDbMsg=Failed to open hash set at {0}. HashLookupModuleFactory.moduleName.text=Hash Lookup HashLookupModuleFactory.moduleDescription.text=Identifies known and notable files using supplied hash sets, such as a standard NSRL hash set. @@ -237,3 +238,5 @@ HashDbCreateDatabaseDialog.lbOrg.text=Source Organization: HashDbCreateDatabaseDialog.orgButton.text=Manage Organizations HashDbCreateDatabaseDialog.databasePathLabel.text=Hash Set Path: AddHashValuesToDatabaseDialog.okButton.text_2=OK +HashDbImportDatabaseDialog.saveInUserConfigFolderCheckbox.text=Copy hash set into user configuration folder +HashDbImportDatabaseDialog.saveInUserConfigFolderCheckbox.toolTipText=In Live Triage situations, this option ensures that path to the hash set will be valid diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbCreateDatabaseDialog.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbCreateDatabaseDialog.java index a20d0bf5a7..fd4978ef09 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbCreateDatabaseDialog.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbCreateDatabaseDialog.java @@ -60,6 +60,7 @@ final class HashDbCreateDatabaseDialog extends javax.swing.JDialog { private final static String LAST_FILE_PATH_KEY = "HashDbCreate_Path"; private EamOrganization selectedOrg = null; private List orgs = null; + static final String HASH_DATABASE_DIR_NAME = "HashDatabases"; /** * Displays a dialog that allows a user to create a new hash database and @@ -404,7 +405,7 @@ final class HashDbCreateDatabaseDialog extends javax.swing.JDialog { private void saveAsButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveAsButtonActionPerformed try { - String lastBaseDirectory = Paths.get(PlatformUtil.getUserConfigDirectory(), "HashDatabases").toString(); + String lastBaseDirectory = Paths.get(PlatformUtil.getUserConfigDirectory(), HASH_DATABASE_DIR_NAME).toString(); if (ModuleSettings.settingExists(ModuleSettings.MAIN_SETTINGS, LAST_FILE_PATH_KEY)) { lastBaseDirectory = ModuleSettings.getConfigSetting(ModuleSettings.MAIN_SETTINGS, LAST_FILE_PATH_KEY); } diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbImportDatabaseDialog.form b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbImportDatabaseDialog.form index fe5cedc889..e285e99a12 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbImportDatabaseDialog.form +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbImportDatabaseDialog.form @@ -29,7 +29,7 @@ - + @@ -86,6 +86,7 @@ + @@ -145,7 +146,9 @@ - + + + @@ -354,5 +357,15 @@ + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbImportDatabaseDialog.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbImportDatabaseDialog.java index d779629846..db27f27365 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbImportDatabaseDialog.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbImportDatabaseDialog.java @@ -27,6 +27,7 @@ import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.filechooser.FileNameExtensionFilter; +import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.openide.util.NbBundle; import org.openide.windows.WindowManager; @@ -180,6 +181,7 @@ final class HashDbImportDatabaseDialog extends javax.swing.JDialog { fileTypeRadioButton = new javax.swing.JRadioButton(); centralRepoRadioButton = new javax.swing.JRadioButton(); jLabel4 = new javax.swing.JLabel(); + saveInUserConfigFolderCheckbox = new javax.swing.JCheckBox(); setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); @@ -286,6 +288,9 @@ final class HashDbImportDatabaseDialog extends javax.swing.JDialog { org.openide.awt.Mnemonics.setLocalizedText(jLabel4, org.openide.util.NbBundle.getMessage(HashDbImportDatabaseDialog.class, "HashDbImportDatabaseDialog.jLabel4.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(saveInUserConfigFolderCheckbox, org.openide.util.NbBundle.getMessage(HashDbImportDatabaseDialog.class, "HashDbImportDatabaseDialog.saveInUserConfigFolderCheckbox.text")); // NOI18N + saveInUserConfigFolderCheckbox.setToolTipText(org.openide.util.NbBundle.getMessage(HashDbImportDatabaseDialog.class, "HashDbImportDatabaseDialog.saveInUserConfigFolderCheckbox.toolTipText")); // NOI18N + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); getContentPane().setLayout(layout); layout.setHorizontalGroup( @@ -334,6 +339,7 @@ final class HashDbImportDatabaseDialog extends javax.swing.JDialog { .addComponent(cancelButton)) .addGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(saveInUserConfigFolderCheckbox) .addComponent(jLabel2) .addComponent(readOnlyCheckbox) .addGroup(layout.createSequentialGroup() @@ -384,7 +390,9 @@ final class HashDbImportDatabaseDialog extends javax.swing.JDialog { .addComponent(readOnlyCheckbox) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(sendIngestMessagesCheckbox) - .addGap(0, 39, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(saveInUserConfigFolderCheckbox) + .addGap(0, 29, Short.MAX_VALUE)) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() .addGap(0, 0, Short.MAX_VALUE) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) @@ -397,7 +405,7 @@ final class HashDbImportDatabaseDialog extends javax.swing.JDialog { }// //GEN-END:initComponents private void openButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_openButtonActionPerformed - String lastBaseDirectory = Paths.get(PlatformUtil.getUserConfigDirectory(), "HashDatabases").toString(); + String lastBaseDirectory = Paths.get(PlatformUtil.getUserConfigDirectory(), HashDbCreateDatabaseDialog.HASH_DATABASE_DIR_NAME).toString(); if (ModuleSettings.settingExists(ModuleSettings.MAIN_SETTINGS, LAST_FILE_PATH_KEY)) { lastBaseDirectory = ModuleSettings.getConfigSetting(ModuleSettings.MAIN_SETTINGS, LAST_FILE_PATH_KEY); } @@ -492,6 +500,7 @@ final class HashDbImportDatabaseDialog extends javax.swing.JDialog { JOptionPane.ERROR_MESSAGE); return; } + File file = new File(selectedFilePath); if (!file.exists()) { JOptionPane.showMessageDialog(this, @@ -503,6 +512,22 @@ final class HashDbImportDatabaseDialog extends javax.swing.JDialog { return; } + if (saveInUserConfigFolderCheckbox.isSelected()) { + // copy the hash database to user configuration directory and use that path instead (JIRA-4177) + String locationInUserConfigDir = Paths.get(PlatformUtil.getUserConfigDirectory(), HashDbCreateDatabaseDialog.HASH_DATABASE_DIR_NAME, hashSetNameTextField.getText(), file.getName()).toString(); + try { + FileUtils.copyFile(file, new File(locationInUserConfigDir)); + // update the hash database location + selectedFilePath = locationInUserConfigDir; + } catch (IOException ex) { + String errorMessage = NbBundle.getMessage(this.getClass(), "HashDbImportDatabaseDialog.unableToCopyToUserDirMsg", locationInUserConfigDir); + Logger.getLogger(HashDbImportDatabaseDialog.class.getName()).log(Level.SEVERE, errorMessage, ex); + JOptionPane.showMessageDialog(this, errorMessage, NbBundle.getMessage(this.getClass(), "HashDbImportDatabaseDialog.importHashDbErr"), + JOptionPane.ERROR_MESSAGE); + return; + } + } + KnownFilesType type; if (knownRadioButton.isSelected()) { type = KnownFilesType.KNOWN; @@ -622,6 +647,7 @@ final class HashDbImportDatabaseDialog extends javax.swing.JDialog { private javax.swing.JButton orgButton; private javax.swing.JComboBox orgComboBox; private javax.swing.JCheckBox readOnlyCheckbox; + private javax.swing.JCheckBox saveInUserConfigFolderCheckbox; private javax.swing.JCheckBox sendIngestMessagesCheckbox; private javax.swing.ButtonGroup storageTypeButtonGroup; private javax.swing.JTextField versionTextField; diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbManager.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbManager.java index f2baef923b..00003da9e7 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbManager.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbManager.java @@ -39,6 +39,7 @@ import org.netbeans.api.progress.ProgressHandle; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; @@ -644,7 +645,7 @@ public class HashDbManager implements PropertyChangeListener { return true; } - private String getValidFilePath(String hashSetName, String configuredPath) { + private String getValidFilePath(String hashSetName, String configuredPath) { // Check the configured path. File database = new File(configuredPath); if (database.exists()) { @@ -1238,8 +1239,8 @@ public class HashDbManager implements PropertyChangeListener { EamGlobalFileInstance fileInstance = new EamGlobalFileInstance(referenceSetID, file.getMd5Hash(), type, comment); EamDb.getInstance().addReferenceInstance(fileInstance,EamDb.getInstance().getCorrelationTypeById(CorrelationAttributeInstance.FILES_TYPE_ID)); - } catch (EamDbException ex){ - throw new TskCoreException("Error adding hashes to " + getDisplayName(), ex); + } catch (EamDbException | CorrelationAttributeNormalizationException ex){ + throw new TskCoreException("Error adding hashes to " + getDisplayName(), ex); //NON-NLS } } } @@ -1264,7 +1265,7 @@ public class HashDbManager implements PropertyChangeListener { } try { globalFileInstances.add(new EamGlobalFileInstance(referenceSetID, hashEntry.getMd5Hash(), type, hashEntry.getComment())); - } catch (EamDbException ex){ + } catch (EamDbException | CorrelationAttributeNormalizationException ex){ throw new TskCoreException("Error adding hashes to " + getDisplayName(), ex); } } @@ -1295,7 +1296,7 @@ public class HashDbManager implements PropertyChangeListener { if (null != file.getMd5Hash()) { try{ return EamDb.getInstance().isFileHashInReferenceSet(file.getMd5Hash(), this.referenceSetID); - } catch (EamDbException ex){ + } catch (EamDbException | CorrelationAttributeNormalizationException ex){ Logger.getLogger(SleuthkitHashSet.class.getName()).log(Level.SEVERE, "Error performing central reposiotry hash lookup for hash " + file.getMd5Hash() + " in reference set " + referenceSetID, ex); //NON-NLS throw new TskCoreException("Error performing central reposiotry hash lookup", ex); @@ -1327,7 +1328,7 @@ public class HashDbManager implements PropertyChangeListener { // Make a bare-bones HashHitInfo for now result = new HashHitInfo(file.getMd5Hash(), "", ""); } - } catch (EamDbException ex){ + } catch (EamDbException | CorrelationAttributeNormalizationException ex){ Logger.getLogger(SleuthkitHashSet.class.getName()).log(Level.SEVERE, "Error performing central reposiotry hash lookup for hash " + file.getMd5Hash() + " in reference set " + referenceSetID, ex); //NON-NLS throw new TskCoreException("Error performing central reposiotry hash lookup", ex); diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettings.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettings.java index 081ff5017c..15407a881a 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettings.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettings.java @@ -62,6 +62,9 @@ final class HashLookupSettings implements Serializable { private static final String CONFIG_FILE_NAME = "hashsets.xml"; //NON-NLS private static final String configFilePath = PlatformUtil.getUserConfigDirectory() + File.separator + CONFIG_FILE_NAME; private static final Logger logger = Logger.getLogger(HashDbManager.class.getName()); + + private static final String USER_DIR_PLACEHOLDER = "[UserConfigFolder]"; + private static final String CURRENT_USER_DIR = PlatformUtil.getUserConfigDirectory(); private static final long serialVersionUID = 1L; private final List hashDbInfoList; @@ -122,10 +125,17 @@ final class HashLookupSettings implements Serializable { * @throws HashLookupSettingsException If there's a problem importing the * settings */ - private static HashLookupSettings readSerializedSettings() throws HashLookupSettingsException { + private static HashLookupSettings readSerializedSettings() throws HashLookupSettingsException { try { try (NbObjectInputStream in = new NbObjectInputStream(new FileInputStream(SERIALIZATION_FILE_PATH))) { HashLookupSettings filesSetsSettings = (HashLookupSettings) in.readObject(); + + /* NOTE: to support JIRA-4177, we need to check if any of the hash + database paths are in Windows user directory. If so, we replace the path + with USER_DIR_PLACEHOLDER before saving to disk. When reading from disk, + USER_DIR_PLACEHOLDER needs to be replaced with current user directory path. + */ + convertPlaceholderToPath(filesSetsSettings); return filesSetsSettings; } } catch (IOException | ClassNotFoundException ex) { @@ -282,8 +292,16 @@ final class HashLookupSettings implements Serializable { */ static boolean writeSettings(HashLookupSettings settings) { + /* NOTE: to support JIRA-4177, we need to check if any of the hash + database paths are in Windows user directory. If so, replace the path + with USER_DIR_PLACEHOLDER so that when it is read, it gets updated to be + the current user directory path. + */ + convertPathToPlaceholder(settings); try (NbObjectOutputStream out = new NbObjectOutputStream(new FileOutputStream(SERIALIZATION_FILE_PATH))) { out.writeObject(settings); + // restore the paths, in case they are going to be used somewhere + convertPlaceholderToPath(settings); return true; } catch (Exception ex) { logger.log(Level.SEVERE, "Could not write hash set settings."); @@ -291,13 +309,54 @@ final class HashLookupSettings implements Serializable { } } + /** + * For file type hash sets, check if hash set paths needs to be modified + * per JIRA-4177. If the file path is in current Windows user directory, + * replace the path with USER_DIR_PLACEHOLDER. + * + * @param settings HashLookupSettings settings object to examiner and modify + */ + static void convertPathToPlaceholder(HashLookupSettings settings) { + for (HashDbInfo hashDbInfo : settings.getHashDbInfo()) { + if (hashDbInfo.isFileDatabaseType()) { + String dbPath = hashDbInfo.getPath(); + if (dbPath.startsWith(CURRENT_USER_DIR)) { + // replace the current user directory with place holder + String remainingPath = dbPath.substring(CURRENT_USER_DIR.length()); + hashDbInfo.setPath(USER_DIR_PLACEHOLDER + remainingPath); + } + } + } + } + + /** + * For file type hash sets, check if hash set paths needs to be modified per + * JIRA-4177. Replace USER_DIR_PLACEHOLDER with path to current Windows user + * directory. + * + * @param settings HashLookupSettings settings object to examiner and modify + */ + static void convertPlaceholderToPath(HashLookupSettings settings) { + for (HashDbInfo hashDbInfo : settings.getHashDbInfo()) { + if (hashDbInfo.isFileDatabaseType()) { + String dbPath = hashDbInfo.getPath(); + if (dbPath.startsWith(USER_DIR_PLACEHOLDER)) { + // replace the place holder with current user directory + String remainingPath = dbPath.substring(USER_DIR_PLACEHOLDER.length()); + hashDbInfo.setPath(CURRENT_USER_DIR + remainingPath); + } + } + } + } + + /** * Represents the serializable information within a hash lookup in order to * be written to disk. Used to hand off information when loading and saving * hash lookups. */ static final class HashDbInfo implements Serializable { - + enum DatabaseType{ FILE, CENTRAL_REPOSITORY @@ -308,7 +367,7 @@ final class HashLookupSettings implements Serializable { private final HashDbManager.HashDb.KnownFilesType knownFilesType; private boolean searchDuringIngest; private final boolean sendIngestMessages; - private final String path; + private String path; private final String version; private final boolean readOnly; private final int referenceSetID; @@ -345,7 +404,7 @@ final class HashLookupSettings implements Serializable { this.searchDuringIngest = searchDuringIngest; this.sendIngestMessages = sendIngestMessages; this.path = ""; - dbType = DatabaseType.CENTRAL_REPOSITORY; + dbType = DatabaseType.CENTRAL_REPOSITORY; } HashDbInfo(HashDbManager.HashDb db) throws TskCoreException{ @@ -445,6 +504,14 @@ final class HashLookupSettings implements Serializable { */ String getPath() { return path; + } + + /** + * Sets the path. + * @param path the path to set + */ + public void setPath(String path) { + this.path = path; } int getReferenceSetID(){ diff --git a/Core/src/org/sleuthkit/autopsy/report/TableReportGenerator.java b/Core/src/org/sleuthkit/autopsy/report/TableReportGenerator.java index 69384bfe0c..722af5494a 100644 --- a/Core/src/org/sleuthkit/autopsy/report/TableReportGenerator.java +++ b/Core/src/org/sleuthkit/autopsy/report/TableReportGenerator.java @@ -1068,13 +1068,17 @@ class TableReportGenerator { orderedRowData.add(makeCommaSeparatedList(getTags())); } else if (BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID() == getArtifact().getArtifactTypeID()) { - String[] attributeDataArray = new String[3]; + String[] attributeDataArray = new String[5]; // Array is used so that order of the attributes is maintained. for (BlackboardAttribute attr : attributes) { if (attr.getAttributeType().equals(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME))) { attributeDataArray[0] = attr.getDisplayString(); } else if (attr.getAttributeType().equals(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_CATEGORY))) { attributeDataArray[1] = attr.getDisplayString(); + } else if (attr.getAttributeType().equals(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT))) { + attributeDataArray[3] = attr.getDisplayString(); + } else if (attr.getAttributeType().equals(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DESCRIPTION))) { + attributeDataArray[4] = attr.getDisplayString(); } } @@ -1163,6 +1167,7 @@ class TableReportGenerator { * * @return List row titles */ + @Messages({"ReportGenerator.artTableColHdr.comment=Comment"}) private List getArtifactTableColumns(int artifactTypeId, Set attributeTypeSet) { ArrayList columns = new ArrayList<>(); @@ -1557,6 +1562,12 @@ class TableReportGenerator { columns.add(new AttributeColumn(NbBundle.getMessage(this.getClass(), "ReportGenerator.artTableColHdr.tskPath"), new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH))); + + columns.add(new AttributeColumn(NbBundle.getMessage(this.getClass(), "ReportGenerator.artTableColHdr.comment"), + new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT))); + + columns.add(new AttributeColumn(NbBundle.getMessage(this.getClass(), "ReportGenerator.artTableColHdr.description"), + new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DESCRIPTION))); } else if (BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_ROUTE.getTypeID() == artifactTypeId) { columns.add(new AttributeColumn(NbBundle.getMessage(this.getClass(), "ReportGenerator.artTableColHdr.tskGpsRouteCategory"), diff --git a/Core/src/org/sleuthkit/autopsy/timeline/PromptDialogManager.java b/Core/src/org/sleuthkit/autopsy/timeline/PromptDialogManager.java index cc58dfd80a..f9f1e12cfa 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/PromptDialogManager.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/PromptDialogManager.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2015-17 Basis Technology Corp. + * Copyright 2015-18 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -56,9 +56,7 @@ public final class PromptDialogManager { @NbBundle.Messages("PrompDialogManager.buttonType.update=Update DB") private static final ButtonType UPDATE = new ButtonType(Bundle.PrompDialogManager_buttonType_update(), ButtonBar.ButtonData.OK_DONE); - /** - * Image to use as title bar icon in dialogs - */ + /** Image to use as title bar icon in dialogs */ private static final Image AUTOPSY_ICON; static { @@ -222,7 +220,7 @@ public final class PromptDialogManager { dialog.setHeaderText(Bundle.PromptDialogManager_showTooManyFiles_headerText()); dialog.showAndWait(); } - + @NbBundle.Messages({ "PromptDialogManager.showTimeLineDisabledMessage.contentText=" + "Timeline functionality is not available yet." 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 543eb59de5..db3c6d18ec 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 @@ -29,7 +29,10 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Iterator; +import java.util.Random; import java.util.stream.Collectors; +import java.util.stream.IntStream; import junit.framework.Test; import junit.framework.TestCase; import org.apache.commons.io.FileUtils; @@ -164,7 +167,6 @@ public class CentralRepoDatamodelTest extends TestCase { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } - } @Override @@ -233,7 +235,7 @@ public class CentralRepoDatamodelTest extends TestCase { for (CorrelationAttributeInstance a : attrs) { assertTrue("Artifact did not have expected BAD status", a.getKnownStatus().equals(TskData.FileKnown.BAD)); } - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -258,10 +260,10 @@ public class CentralRepoDatamodelTest extends TestCase { } else if (case2.getCaseUUID().equals(a.getCorrelationCase().getCaseUUID())) { assertTrue("Artifact did not have expected KNOWN status", a.getKnownStatus().equals(TskData.FileKnown.KNOWN)); } else { - Assert.fail("getArtifactInstancesByTypeValue returned unexpected case"); + fail("getArtifactInstancesByTypeValue returned unexpected case"); } } - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -278,71 +280,79 @@ public class CentralRepoDatamodelTest extends TestCase { List attrs = EamDb.getInstance().getArtifactInstancesByTypeValue(fileType, hashToChangeToNotable); assertTrue("getArtifactInstancesByTypeValue returned " + attrs.size() + " values - expected 1", attrs.size() == 1); assertTrue("Artifact status did not change to BAD", attrs.get(0).getKnownStatus().equals(TskData.FileKnown.BAD)); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } // Try to update artifact with two CorrelationAttributeInstance instances - // This appears to no longer be a valid test, already moved fail to the catch since it no longe fails. try { - CorrelationAttributeInstance attr1 = new CorrelationAttributeInstance("badHash", fileType, case1, dataSource1fromCase1, "badPath", + CorrelationAttributeInstance attr1 = new CorrelationAttributeInstance(randomHash(), fileType, case1, dataSource1fromCase1, BAD_PATH, "", TskData.FileKnown.KNOWN); - CorrelationAttributeInstance attr2 = new CorrelationAttributeInstance("badHash", fileType, case1, dataSource1fromCase2, "badPath", + CorrelationAttributeInstance attr2 = new CorrelationAttributeInstance(randomHash(), fileType, case1, dataSource1fromCase2, BAD_PATH, "", TskData.FileKnown.KNOWN); EamDb.getInstance().setAttributeInstanceKnownStatus(attr1, TskData.FileKnown.BAD); EamDb.getInstance().setAttributeInstanceKnownStatus(attr2, TskData.FileKnown.BAD); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Assert.fail("setArtifactInstanceKnownStatus threw an exception for sequential Correlation Attribute Instances updates"); } // Try to update null artifact try { EamDb.getInstance().setAttributeInstanceKnownStatus(null, TskData.FileKnown.BAD); - Assert.fail("setArtifactInstanceKnownStatus failed to throw exception for null correlation attribute"); + fail("setArtifactInstanceKnownStatus failed to throw exception for null correlation attribute"); } catch (EamDbException ex) { // This is the expected behavior } // Try to update artifact with null known status try { - CorrelationAttributeInstance attr = new CorrelationAttributeInstance("badHash", fileType, case1, dataSource1fromCase1, "badPath", + CorrelationAttributeInstance attr = new CorrelationAttributeInstance(randomHash(), fileType, case1, dataSource1fromCase1, BAD_PATH, "", TskData.FileKnown.KNOWN); EamDb.getInstance().setAttributeInstanceKnownStatus(attr, null); - Assert.fail("setArtifactInstanceKnownStatus failed to throw exception for null known status"); + fail("setArtifactInstanceKnownStatus failed to throw exception for null known status"); } catch (EamDbException ex) { // This is the expected behavior + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); } // Try to update artifact with null case try { - CorrelationAttributeInstance attr = new CorrelationAttributeInstance("badHash", fileType, null, dataSource1fromCase1, "badPath", + CorrelationAttributeInstance attr = new CorrelationAttributeInstance(randomHash(), fileType, null, dataSource1fromCase1, BAD_PATH, "", TskData.FileKnown.KNOWN); EamDb.getInstance().setAttributeInstanceKnownStatus(attr, TskData.FileKnown.BAD); - Assert.fail("setArtifactInstanceKnownStatus failed to throw exception for null case"); + fail("setArtifactInstanceKnownStatus failed to throw exception for null case"); } catch (EamDbException ex) { // This is the expected behavior + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); } // Try to update artifact with null data source try { - CorrelationAttributeInstance attr = new CorrelationAttributeInstance("badHash", fileType, case1, null, "badPath", + CorrelationAttributeInstance attr = new CorrelationAttributeInstance(randomHash(), fileType, case1, null, BAD_PATH, "", TskData.FileKnown.KNOWN); EamDb.getInstance().setAttributeInstanceKnownStatus(attr, TskData.FileKnown.BAD); - Assert.fail("setArtifactInstanceKnownStatus failed to throw exception for null case"); + fail("setArtifactInstanceKnownStatus failed to throw exception for null case"); } catch (EamDbException ex) { // This is the expected behavior + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); } // Test getting two notable instances try { List attrs = EamDb.getInstance().getArtifactInstancesKnownBad(fileType, notableHashInBothCases); assertTrue("getArtifactInstancesKnownBad returned " + attrs.size() + " values - expected 2", attrs.size() == 2); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -351,7 +361,7 @@ public class CentralRepoDatamodelTest extends TestCase { try { List attrs = EamDb.getInstance().getArtifactInstancesKnownBad(fileType, notableHashInOneCaseKnownOther); assertTrue("getArtifactInstancesKnownBad returned " + attrs.size() + " values - expected 1", attrs.size() == 1); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -359,15 +369,22 @@ public class CentralRepoDatamodelTest extends TestCase { // Test getting notable instances with null type try { EamDb.getInstance().getArtifactInstancesKnownBad(null, notableHashInOneCaseKnownOther); - Assert.fail("getArtifactInstancesKnownBad failed to throw exception for null type"); - } catch (EamDbException ex) { + fail("getArtifactInstancesKnownBad failed to throw exception for null type"); + } catch (CorrelationAttributeNormalizationException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + fail("should have got CentralRepoValidationException"); } - // Test getting notable instances with null value (should work fine) + // Test getting notable instances with null value try { - List attrs = EamDb.getInstance().getArtifactInstancesKnownBad(fileType, null); - assertTrue("getArtifactInstancesKnownBad returned " + attrs.size() + " values - expected ", attrs.isEmpty()); + EamDb.getInstance().getArtifactInstancesKnownBad(fileType, null); + fail("should get an exception for null inout"); + } catch (CorrelationAttributeNormalizationException ex) { + //this is expecpted + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } catch (EamDbException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); @@ -377,7 +394,7 @@ public class CentralRepoDatamodelTest extends TestCase { try { long count = EamDb.getInstance().getCountArtifactInstancesKnownBad(fileType, notableHashInBothCases); assertTrue("getCountArtifactInstancesKnownBad returned " + count + " values - expected 2", count == 2); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -386,7 +403,7 @@ public class CentralRepoDatamodelTest extends TestCase { try { long count = EamDb.getInstance().getCountArtifactInstancesKnownBad(fileType, notableHashInOneCaseKnownOther); assertTrue("getCountArtifactInstancesKnownBad returned " + count + " values - expected 1", count == 1); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -394,25 +411,31 @@ public class CentralRepoDatamodelTest extends TestCase { // Test getting notable instance count with null type try { EamDb.getInstance().getCountArtifactInstancesKnownBad(null, notableHashInOneCaseKnownOther); - Assert.fail("getCountArtifactInstancesKnownBad failed to throw exception for null type"); - } catch (EamDbException ex) { - // This is the expected behavior - } - - // Test getting notable instance count with null value (should work fine) - try { - long count = EamDb.getInstance().getCountArtifactInstancesKnownBad(fileType, null); - assertTrue("getCountArtifactInstancesKnownBad returned " + count + " values - expected ", count == 0); + fail("getCountArtifactInstancesKnownBad failed to throw exception for null type"); } catch (EamDbException ex) { Exceptions.printStackTrace(ex); - Assert.fail(ex.getMessage()); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex){ + // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } + + // Test getting notable instance count with null value (should throw an exception) + try { + EamDb.getInstance().getCountArtifactInstancesKnownBad(fileType, null); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex){ + // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting cases with notable instances (all instances are notable) try { List cases = EamDb.getInstance().getListCasesHavingArtifactInstancesKnownBad(fileType, notableHashInBothCases); assertTrue("getListCasesHavingArtifactInstancesKnownBad returned " + cases.size() + " values - expected 2", cases.size() == 2); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -422,7 +445,7 @@ public class CentralRepoDatamodelTest extends TestCase { List cases = EamDb.getInstance().getListCasesHavingArtifactInstancesKnownBad(fileType, notableHashInOneCaseKnownOther); assertTrue("getListCasesHavingArtifactInstancesKnownBad returned " + cases.size() + " values - expected 1", cases.size() == 1); assertTrue("getListCasesHavingArtifactInstancesKnownBad returned unexpected case " + cases.get(0), case1.getDisplayName().equals(cases.get(0))); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -430,20 +453,28 @@ public class CentralRepoDatamodelTest extends TestCase { // Test getting cases with null type try { EamDb.getInstance().getListCasesHavingArtifactInstancesKnownBad(null, notableHashInOneCaseKnownOther); - Assert.fail("getListCasesHavingArtifactInstancesKnownBad failed to throw exception for null type"); + fail("getListCasesHavingArtifactInstancesKnownBad failed to throw exception for null type"); } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex){ // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } - // Test getting cases with null value (should work fine) + // Test getting cases with null value (should throw exception) try { List cases = EamDb.getInstance().getListCasesHavingArtifactInstancesKnownBad(fileType, null); assertTrue("getListCasesHavingArtifactInstancesKnownBad returned " + cases.size() + " values - expected ", cases.isEmpty()); } catch (EamDbException ex) { Exceptions.printStackTrace(ex); - Assert.fail(ex.getMessage()); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex){ + // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } } + private static final String BAD_PATH = "badPath"; /** * Test the methods associated with bulk artifacts (addAttributeInstanceBulk and @@ -476,7 +507,7 @@ public class CentralRepoDatamodelTest extends TestCase { // Create the first list, which will have (bulkThreshold / 2) entries List list1 = new ArrayList<>(); for (int i = 0; i < DEFAULT_BULK_THRESHOLD / 2; i++) { - String value = "bulkInsertValue1_" + String.valueOf(i); + String value = randomHash(); String path = "C:\\bulkInsertPath1\\file" + String.valueOf(i); CorrelationAttributeInstance attr = new CorrelationAttributeInstance(value, fileType, case1, dataSource1fromCase1, path); @@ -495,7 +526,7 @@ public class CentralRepoDatamodelTest extends TestCase { // Make a second list with length equal to bulkThreshold List list2 = new ArrayList<>(); for (int i = 0; i < DEFAULT_BULK_THRESHOLD; i++) { - String value = "bulkInsertValue2_" + String.valueOf(i); + String value = randomHash(); String path = "C:\\bulkInsertPath2\\file" + String.valueOf(i); CorrelationAttributeInstance attr = new CorrelationAttributeInstance(value, fileType, case1, dataSource1fromCase1, path); @@ -517,57 +548,77 @@ public class CentralRepoDatamodelTest extends TestCase { int expectedCount = list1.size() + list2.size(); assertTrue("Artifact count " + count + " does not match expected count " + expectedCount, count == expectedCount); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } // Test preparing artifact with null type try { - CorrelationAttributeInstance attr = new CorrelationAttributeInstance(null, "value"); + CorrelationAttributeInstance attr = new CorrelationAttributeInstance(null, randomHash()); EamDb.getInstance().addAttributeInstanceBulk(attr); - Assert.fail("prepareBulkArtifact failed to throw exception for null type"); + fail("prepareBulkArtifact failed to throw exception for null type"); } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test preparing artifact with null case try { - CorrelationAttributeInstance attr = new CorrelationAttributeInstance("value", fileType, null, dataSource1fromCase1, "path"); + CorrelationAttributeInstance attr = new CorrelationAttributeInstance(randomHash(), fileType, null, dataSource1fromCase1, "path"); EamDb.getInstance().addAttributeInstanceBulk(attr); EamDb.getInstance().commitAttributeInstancesBulk(); - Assert.fail("bulkInsertArtifacts failed to throw exception for null case"); + fail("bulkInsertArtifacts failed to throw exception for null case"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); } // Test preparing artifact with null data source try { - CorrelationAttributeInstance attr = new CorrelationAttributeInstance("value", fileType, case1, null, "path"); + CorrelationAttributeInstance attr = new CorrelationAttributeInstance(randomHash(), fileType, case1, null, "path"); EamDb.getInstance().addAttributeInstanceBulk(attr); EamDb.getInstance().commitAttributeInstancesBulk(); - Assert.fail("prepareBulkArtifact failed to throw exception for null data source"); + fail("prepareBulkArtifact failed to throw exception for null data source"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); } // Test preparing artifact with null path // CorrelationAttributeInstance will throw an exception try { - CorrelationAttributeInstance attr = new CorrelationAttributeInstance("value", fileType, case1, dataSource1fromCase1, null); - Assert.fail("CorrelationAttributeInstance failed to throw exception for null path"); + new CorrelationAttributeInstance(randomHash(), fileType, case1, dataSource1fromCase1, null); + fail("CorrelationAttributeInstance failed to throw exception for null path"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); } // Test preparing artifact with null known status try { - CorrelationAttributeInstance attr = new CorrelationAttributeInstance("value", fileType, case1, dataSource1fromCase1, "path", "comment", null); + CorrelationAttributeInstance attr = new CorrelationAttributeInstance(randomHash(), fileType, case1, dataSource1fromCase1, "path", "comment", null); EamDb.getInstance().addAttributeInstanceBulk(attr); EamDb.getInstance().commitAttributeInstancesBulk(); - Assert.fail("prepareBulkArtifact failed to throw exception for null known status"); + fail("prepareBulkArtifact failed to throw exception for null known status"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); } } @@ -659,7 +710,7 @@ public class CentralRepoDatamodelTest extends TestCase { try { CorrelationAttributeInstance attr = new CorrelationAttributeInstance(onlyInDataSource3Hash, fileType, case2, dataSource1fromCase2, onlyInDataSource3Path); EamDb.getInstance().addArtifactInstance(attr); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -673,7 +724,7 @@ public class CentralRepoDatamodelTest extends TestCase { CorrelationAttributeInstance attr3 = new CorrelationAttributeInstance(inAllDataSourcesHash, fileType, case2, dataSource1fromCase2, inAllDataSourcesPath); EamDb.getInstance().addArtifactInstance(attr3); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -685,7 +736,7 @@ public class CentralRepoDatamodelTest extends TestCase { CorrelationAttributeInstance attr2 = new CorrelationAttributeInstance(inDataSource1twiceHash, fileType, case1, dataSource1fromCase1, inDataSource1twicePath2); EamDb.getInstance().addArtifactInstance(attr2); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -695,7 +746,7 @@ public class CentralRepoDatamodelTest extends TestCase { try { CorrelationAttributeInstance attr = new CorrelationAttributeInstance(emailValue, emailType, case1, dataSource1fromCase1, emailPath); EamDb.getInstance().addArtifactInstance(attr); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -708,7 +759,7 @@ public class CentralRepoDatamodelTest extends TestCase { case1, dataSource1fromCase1, phonePath); EamDb.getInstance().addArtifactInstance(attr); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -720,7 +771,7 @@ public class CentralRepoDatamodelTest extends TestCase { EamDb.getInstance().getCorrelationTypeById(CorrelationAttributeInstance.DOMAIN_TYPE_ID), case1, dataSource1fromCase1, domainPath); EamDb.getInstance().addArtifactInstance(attr); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -733,95 +784,117 @@ public class CentralRepoDatamodelTest extends TestCase { case1, dataSource1fromCase1, devIdPath); EamDb.getInstance().addArtifactInstance(attr); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } - // Test CorrelationAttributeInstance failure cases - // Create an attribute to use in the next few tests - CorrelationAttributeInstance failAttr; + // Test CorrelationAttributeInstance creation try { - failAttr = new CorrelationAttributeInstance(fileType, "badInstances"); - } catch (EamDbException ex) { + new CorrelationAttributeInstance(fileType, randomHash()); + } catch (CorrelationAttributeNormalizationException | EamDbException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); - return; } // Test adding instance with null case try { - CorrelationAttributeInstance failAttrInst = new CorrelationAttributeInstance("badInstances", fileType, null, dataSource1fromCase2, "badPath"); + CorrelationAttributeInstance failAttrInst = new CorrelationAttributeInstance("badInstances", fileType, null, dataSource1fromCase2, BAD_PATH); EamDb.getInstance().addArtifactInstance(failAttrInst); - Assert.fail("addArtifact failed to throw exception for null case"); + fail("addArtifact failed to throw exception for null case"); } catch (EamDbException ex) { + fail("was expecting to get CorrelationAttributeNormalizationException"); + } catch (CorrelationAttributeNormalizationException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test adding instance with invalid case ID try { CorrelationCase badCase = new CorrelationCase("badCaseUuid", "badCaseName"); - CorrelationAttributeInstance failAttrInst2 = new CorrelationAttributeInstance("badInstances", fileType, badCase, dataSource1fromCase2, "badPath"); + CorrelationAttributeInstance failAttrInst2 = new CorrelationAttributeInstance(randomHash(), fileType, badCase, dataSource1fromCase2, BAD_PATH); EamDb.getInstance().addArtifactInstance(failAttrInst2); - Assert.fail("addArtifact failed to throw exception for invalid case"); + fail("addArtifact failed to throw exception for invalid case"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } catch (CorrelationAttributeNormalizationException ex) { + fail("was expecting to get EamDbException"); } // Test adding instance with null data source try { - CorrelationAttributeInstance failAttrInst3 = new CorrelationAttributeInstance("badInstances", fileType, case1, null, "badPath"); + CorrelationAttributeInstance failAttrInst3 = new CorrelationAttributeInstance(randomHash(), fileType, case1, null, BAD_PATH); EamDb.getInstance().addArtifactInstance(failAttrInst3); - Assert.fail("addArtifact failed to throw exception for null data source"); + fail("addArtifact failed to throw exception for null data source"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } catch (CorrelationAttributeNormalizationException ex) { + fail("was expecting to get EamDbException"); } // Test adding instance with invalid data source ID try { CorrelationDataSource badDS = new CorrelationDataSource(case1, "badDSUuid", "badDSName"); - CorrelationAttributeInstance failAttrInst4 = new CorrelationAttributeInstance("badInstances", fileType, case1, badDS, "badPath"); + CorrelationAttributeInstance failAttrInst4 = new CorrelationAttributeInstance(randomHash(), fileType, case1, badDS, BAD_PATH); EamDb.getInstance().addArtifactInstance(failAttrInst4); - Assert.fail("addArtifact failed to throw exception for invalid data source"); + fail("addArtifact failed to throw exception for invalid data source"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } catch (CorrelationAttributeNormalizationException ex) { + fail("was expecting to get EamDbException"); } // Test adding instance with null path // This will fail in the CorrelationAttributeInstance constructor try { - new CorrelationAttributeInstance("badInstances", fileType, case1, dataSource1fromCase1, null); - Assert.fail("CorrelationAttributeInstance failed to throw exception for null path"); + new CorrelationAttributeInstance(randomHash(), fileType, case1, dataSource1fromCase1, null); + fail("CorrelationAttributeInstance failed to throw exception for null path"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } catch (CorrelationAttributeNormalizationException ex) { + fail("was expecting to get EamDbException"); } // Test adding instance with null known status try { CorrelationAttributeInstance failAttrInst5 = new CorrelationAttributeInstance("badInstances", fileType, case1, dataSource1fromCase1, null, "comment", null); EamDb.getInstance().addArtifactInstance(failAttrInst5); - Assert.fail("addArtifact failed to throw exception for null known status"); + fail("addArtifact failed to throw exception for null known status"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } catch (CorrelationAttributeNormalizationException ex) { + fail("was expecting to get EamDbException"); } // Test CorrelationAttribute failure cases // Test null type try { - CorrelationAttributeInstance attr = new CorrelationAttributeInstance(null, "badInstances"); + CorrelationAttributeInstance attr = new CorrelationAttributeInstance(null, randomHash()); EamDb.getInstance().addArtifactInstance(attr); - Assert.fail("addArtifact failed to throw exception for null type"); + fail("addArtifact failed to throw exception for null type"); } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test null value // This will fail in the CorrelationAttribute constructor try { new CorrelationAttributeInstance(fileType, null); - Assert.fail("addArtifactInsance failed to throw exception for null value"); - } catch (EamDbException ex) { + fail("addArtifact failed to throw exception for null value"); + } catch (CorrelationAttributeNormalizationException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } catch (EamDbException ex) { + fail("expected to get CorrelationAttributeNormalizationException"); } // Test getting instances with expected results @@ -834,37 +907,45 @@ public class CentralRepoDatamodelTest extends TestCase { assertTrue("getArtifactInstancesByTypeValue returned instance with unexpected path " + inst.getFilePath(), inAllDataSourcesPath.equalsIgnoreCase(inst.getFilePath())); } - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } - // Test getting instances expecting no results + // Test getting instances with mismatched data / data-type and expect an exception try { - List instances = EamDb.getInstance().getArtifactInstancesByTypeValue( - emailType, inAllDataSourcesHash); - assertTrue("getArtifactInstancesByTypeValue returned " + instances.size() + " results - expected 0", instances.isEmpty()); + EamDb.getInstance().getArtifactInstancesByTypeValue(emailType, inAllDataSourcesHash); + fail("we should get an exception"); } catch (EamDbException ex) { Exceptions.printStackTrace(ex); - Assert.fail(ex.getMessage()); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex){ + //this is expected + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting instances with null type try { EamDb.getInstance().getArtifactInstancesByTypeValue(null, inAllDataSourcesHash); - Assert.fail("getArtifactInstancesByTypeValue failed to throw exception for null type"); + fail("getArtifactInstancesByTypeValue failed to throw exception for null type"); } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex){ // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting instances with null value - // Should just return nothing try { - List instances = EamDb.getInstance().getArtifactInstancesByTypeValue(fileType, null); - assertTrue("getArtifactInstancesByTypeValue returned non-empty list for null value", instances.isEmpty()); + EamDb.getInstance().getArtifactInstancesByTypeValue(fileType, null); + fail("this should produce an exception"); } catch (EamDbException ex) { Exceptions.printStackTrace(ex); - Assert.fail(ex.getMessage()); + fail(ex.getMessage()); + } catch(CorrelationAttributeNormalizationException ex){ + //this is expected + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting instances with path that should produce results @@ -888,33 +969,35 @@ public class CentralRepoDatamodelTest extends TestCase { // Test getting instances with null type try { EamDb.getInstance().getArtifactInstancesByPath(null, inAllDataSourcesPath); - Assert.fail("getArtifactInstancesByPath failed to throw exception for null type"); + fail("getArtifactInstancesByPath failed to throw exception for null type"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting instances with null path try { EamDb.getInstance().getArtifactInstancesByPath(fileType, null); - Assert.fail("getArtifactInstancesByPath failed to throw exception for null path"); + fail("getArtifactInstancesByPath failed to throw exception for null path"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting instance count with path that should produce results try { long count = EamDb.getInstance().getCountArtifactInstancesByTypeValue(fileType, inAllDataSourcesHash); assertTrue("getCountArtifactInstancesByTypeValue returned " + count + " - expected 3", count == 3); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } // Test getting instance count with path that should not produce results try { - long count = EamDb.getInstance().getCountArtifactInstancesByTypeValue(fileType, "xyz"); + long count = EamDb.getInstance().getCountArtifactInstancesByTypeValue(fileType, randomHash()); assertTrue("getCountArtifactInstancesByTypeValue returned " + count + " - expected 0", count == 0); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -922,17 +1005,25 @@ public class CentralRepoDatamodelTest extends TestCase { // Test getting instance count with null type try { EamDb.getInstance().getCountArtifactInstancesByTypeValue(null, inAllDataSourcesHash); - Assert.fail("getCountArtifactInstancesByTypeValue failed to throw exception for null type"); + fail("getCountArtifactInstancesByTypeValue failed to throw exception for null type"); } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } catch(CorrelationAttributeNormalizationException ex){ // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting instance count with null value try { EamDb.getInstance().getCountArtifactInstancesByTypeValue(fileType, null); - Assert.fail("getCountArtifactInstancesByTypeValue failed to throw exception for null value"); + fail("getCountArtifactInstancesByTypeValue failed to throw exception for null value"); } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } catch(CorrelationAttributeNormalizationException ex){ // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting frequency of value that is in all three data sources @@ -940,7 +1031,7 @@ public class CentralRepoDatamodelTest extends TestCase { CorrelationAttributeInstance attr = new CorrelationAttributeInstance(fileType, inAllDataSourcesHash); int freq = EamDb.getInstance().getFrequencyPercentage(attr); assertTrue("getFrequencyPercentage returned " + freq + " - expected 100", freq == 100); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -950,7 +1041,7 @@ public class CentralRepoDatamodelTest extends TestCase { CorrelationAttributeInstance attr = new CorrelationAttributeInstance(fileType, inDataSource1twiceHash); int freq = EamDb.getInstance().getFrequencyPercentage(attr); assertTrue("getFrequencyPercentage returned " + freq + " - expected 33", freq == 33); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -960,17 +1051,17 @@ public class CentralRepoDatamodelTest extends TestCase { CorrelationAttributeInstance attr = new CorrelationAttributeInstance(emailType, emailValue); int freq = EamDb.getInstance().getFrequencyPercentage(attr); assertTrue("getFrequencyPercentage returned " + freq + " - expected 33", freq == 33); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } // Test getting frequency of non-existent value try { - CorrelationAttributeInstance attr = new CorrelationAttributeInstance(fileType, "randomValue"); + CorrelationAttributeInstance attr = new CorrelationAttributeInstance(fileType, randomHash()); int freq = EamDb.getInstance().getFrequencyPercentage(attr); assertTrue("getFrequencyPercentage returned " + freq + " - expected 0", freq == 0); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -979,17 +1070,22 @@ public class CentralRepoDatamodelTest extends TestCase { try { CorrelationAttributeInstance attr = new CorrelationAttributeInstance(null, "randomValue"); EamDb.getInstance().getFrequencyPercentage(attr); - Assert.fail("getFrequencyPercentage failed to throw exception for null type"); - } catch (EamDbException ex) { + fail("getFrequencyPercentage failed to throw exception for null type"); + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting frequency with null attribute try { EamDb.getInstance().getFrequencyPercentage(null); - Assert.fail("getFrequencyPercentage failed to throw exception for null attribute"); + fail("getFrequencyPercentage failed to throw exception for null attribute"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); } // Test updating a correlation attribute instance comment @@ -1006,7 +1102,7 @@ public class CentralRepoDatamodelTest extends TestCase { usbDeviceType, case1, dataSource1fromCase1, devIdValue, devIdPath); assertEquals("updateAttributeInstanceComment did not set comment to \"new comment\".", "new comment", correlationAttribute.getComment()); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -1042,7 +1138,7 @@ public class CentralRepoDatamodelTest extends TestCase { try { long count = EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(fileType, inAllDataSourcesHash); assertTrue("getCountUniqueCaseDataSourceTuplesHavingTypeValue returned " + count + " - expected 3", count == 3); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -1051,35 +1147,42 @@ public class CentralRepoDatamodelTest extends TestCase { try { long count = EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(fileType, inDataSource1twiceHash); assertTrue("getCountUniqueCaseDataSourceTuplesHavingTypeValue returned " + count + " - expected 1", count == 1); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } // Test getting data source count for entry that is not in any data sources try { - long count = EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(fileType, "abcdef"); + long count = EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(fileType, randomHash()); assertTrue("getCountUniqueCaseDataSourceTuplesHavingTypeValue returned " + count + " - expected 0", count == 0); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } // Test getting data source count for null type try { - EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(null, "abcdef"); - Assert.fail("getCountUniqueCaseDataSourceTuplesHavingTypeValue failed to throw exception for null type"); + EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(null, randomHash()); + fail("getCountUniqueCaseDataSourceTuplesHavingTypeValue failed to throw exception for null type"); } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting data source count for null value try { - long count = EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(fileType, null); - assertTrue("getCountUniqueCaseDataSourceTuplesHavingTypeValue returned " + count + " - expected 0", count == 0); + EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(fileType, null); + fail("we should get an exception here"); } catch (EamDbException ex) { Exceptions.printStackTrace(ex); - Assert.fail(ex.getMessage()); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex) { + //this is expected + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test running processinstance which queries all rows from instances table @@ -1096,16 +1199,17 @@ public class CentralRepoDatamodelTest extends TestCase { int count2 = instancetableCallback.getCounterNamingConvention(); assertTrue("Process Instance count with filepath naming convention: " + count2 + "-expected 2", count2 == 2); assertTrue("Process Instance count with filepath without naming convention: " + count1 + "-expected greater than 0", count1 > 0); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); } try { //test null inputs EamDb.getInstance().processInstanceTable(null, null); - Assert.fail("processinstance method failed to throw exception for null type value"); + fail("processinstance method failed to throw exception for null type value"); } catch (EamDbException ex) { - // This is the expected + // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test running processinstance which queries all rows from instances table @@ -1122,16 +1226,17 @@ public class CentralRepoDatamodelTest extends TestCase { int count2 = instancetableCallback.getCounterNamingConvention(); assertTrue("Process Instance count with filepath naming convention: " + count2 + "-expected 2", count2 == 2); assertTrue("Process Instance count with filepath without naming convention: " + count1 + "-expected greater than 0", count1 > 0); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); } try { //test null inputs EamDb.getInstance().processInstanceTableWhere(null, null, null); - Assert.fail("processinstance method failed to throw exception for null type value"); + fail("processinstance method failed to throw exception for null type value"); } catch (EamDbException ex) { - // This is the expected + // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } } @@ -1178,18 +1283,20 @@ public class CentralRepoDatamodelTest extends TestCase { try { CorrelationAttributeInstance.Type temp = new CorrelationAttributeInstance.Type(customTypeName, customTypeDb, false, false); EamDb.getInstance().newCorrelationType(temp); - Assert.fail("newCorrelationType failed to throw exception for duplicate name/db table"); + fail("newCorrelationType failed to throw exception for duplicate name/db table"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test new type with null name try { CorrelationAttributeInstance.Type temp = new CorrelationAttributeInstance.Type(null, "temp_type", false, false); EamDb.getInstance().newCorrelationType(temp); - Assert.fail("newCorrelationType failed to throw exception for null name table"); + fail("newCorrelationType failed to throw exception for null name table"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test new type with null db name @@ -1199,14 +1306,16 @@ public class CentralRepoDatamodelTest extends TestCase { Assert.fail("CorrelationAttributeInstance.Type failed to throw exception for null db table name"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test new type with null type try { EamDb.getInstance().newCorrelationType(null); - Assert.fail("newCorrelationType failed to throw exception for null type"); + fail("newCorrelationType failed to throw exception for null type"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting all correlation types @@ -1255,9 +1364,10 @@ public class CentralRepoDatamodelTest extends TestCase { // Test getting the type with a invalid ID try { EamDb.getInstance().getCorrelationTypeById(5555); - Assert.fail("getCorrelationTypeById failed to throw exception for invalid ID"); + fail("getCorrelationTypeById failed to throw exception for invalid ID"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test updating a valid type @@ -1298,18 +1408,20 @@ public class CentralRepoDatamodelTest extends TestCase { try { customType.setDisplayName(null); EamDb.getInstance().updateCorrelationType(customType); - Assert.fail("updateCorrelationType failed to throw exception for null name"); + fail("updateCorrelationType failed to throw exception for null name"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test updating a null type try { customType.setDisplayName(null); EamDb.getInstance().updateCorrelationType(customType); - Assert.fail("updateCorrelationType failed to throw exception for null type"); + fail("updateCorrelationType failed to throw exception for null type"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } } @@ -1373,26 +1485,29 @@ public class CentralRepoDatamodelTest extends TestCase { try { EamOrganization temp = new EamOrganization(orgAname); EamDb.getInstance().newOrganization(temp); - Assert.fail("newOrganization failed to throw exception for duplicate org name"); + fail("newOrganization failed to throw exception for duplicate org name"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test adding null organization try { EamDb.getInstance().newOrganization(null); - Assert.fail("newOrganization failed to throw exception for null org"); + fail("newOrganization failed to throw exception for null org"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test adding organization with null name try { EamOrganization temp = new EamOrganization(null); EamDb.getInstance().newOrganization(temp); - Assert.fail("newOrganization failed to throw exception for null name"); + fail("newOrganization failed to throw exception for null name"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting organizations @@ -1422,9 +1537,10 @@ public class CentralRepoDatamodelTest extends TestCase { // Test getting org with invalid ID try { EamDb.getInstance().getOrganizationByID(12345); - Assert.fail("getOrganizationByID failed to throw exception for invalid ID"); + fail("getOrganizationByID failed to throw exception for invalid ID"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test updating valid org @@ -1457,17 +1573,19 @@ public class CentralRepoDatamodelTest extends TestCase { try { EamOrganization temp = new EamOrganization("invalidOrg"); EamDb.getInstance().updateOrganization(temp); - Assert.fail("updateOrganization worked for invalid ID"); + fail("updateOrganization worked for invalid ID"); } catch (EamDbException ex) { // this is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test updating null org try { EamDb.getInstance().updateOrganization(null); - Assert.fail("updateOrganization failed to throw exception for null org"); + fail("updateOrganization failed to throw exception for null org"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test updating org to null name @@ -1475,9 +1593,10 @@ public class CentralRepoDatamodelTest extends TestCase { EamOrganization copyOfA = EamDb.getInstance().getOrganizationByID(orgA.getOrgID()); copyOfA.setName(null); EamDb.getInstance().updateOrganization(copyOfA); - Assert.fail("updateOrganization failed to throw exception for null name"); + fail("updateOrganization failed to throw exception for null name"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test deleting existing org that isn't in use @@ -1505,26 +1624,29 @@ public class CentralRepoDatamodelTest extends TestCase { // It should now throw an exception if we try to delete it EamDb.getInstance().deleteOrganization(inUseOrg); - Assert.fail("deleteOrganization failed to throw exception for in use organization"); + fail("deleteOrganization failed to throw exception for in use organization"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test deleting non-existent org try { EamOrganization temp = new EamOrganization("temp"); EamDb.getInstance().deleteOrganization(temp); - Assert.fail("deleteOrganization failed to throw exception for non-existent organization"); + fail("deleteOrganization failed to throw exception for non-existent organization"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test deleting null org try { EamDb.getInstance().deleteOrganization(null); - Assert.fail("deleteOrganization failed to throw exception for null organization"); + fail("deleteOrganization failed to throw exception for null organization"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } } @@ -1626,7 +1748,7 @@ public class CentralRepoDatamodelTest extends TestCase { temp = new EamGlobalFileInstance(knownSet1id, knownHash1, TskData.FileKnown.KNOWN, "comment5"); EamDb.getInstance().addReferenceInstance(temp, fileType); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -1635,9 +1757,13 @@ public class CentralRepoDatamodelTest extends TestCase { try { EamGlobalFileInstance temp = new EamGlobalFileInstance(2345, inAllSetsHash, TskData.FileKnown.BAD, "comment"); EamDb.getInstance().addReferenceInstance(temp, fileType); - Assert.fail("addReferenceInstance failed to throw exception for invalid ID"); + fail("addReferenceInstance failed to throw exception for invalid ID"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); } // Test creating file instance with null hash @@ -1645,9 +1771,13 @@ public class CentralRepoDatamodelTest extends TestCase { // call addReferenceInstance and just test the EamGlobalFileInstance constructor try { new EamGlobalFileInstance(notableSet1id, null, TskData.FileKnown.BAD, "comment"); - Assert.fail("EamGlobalFileInstance failed to throw exception for null hash"); + fail("EamGlobalFileInstance failed to throw exception for null hash"); } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test adding file instance with null known status @@ -1655,27 +1785,34 @@ public class CentralRepoDatamodelTest extends TestCase { // call addReferenceInstance and just test the EamGlobalFileInstance constructor try { new EamGlobalFileInstance(notableSet1id, inAllSetsHash, null, "comment"); - Assert.fail("EamGlobalFileInstance failed to throw exception for null type"); + fail("EamGlobalFileInstance failed to throw exception for null type"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); } // Test adding file instance with null correlation type try { EamGlobalFileInstance temp = new EamGlobalFileInstance(notableSet1id, inAllSetsHash, TskData.FileKnown.BAD, "comment"); EamDb.getInstance().addReferenceInstance(temp, null); - Assert.fail("addReferenceInstance failed to throw exception for null type"); + fail("addReferenceInstance failed to throw exception for null type"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); } // Test bulk insert with large valid set try { // Create a list of global file instances. Make enough that the bulk threshold should be hit once. Set instances = new HashSet<>(); - String bulkTestHash = "bulktesthash_"; for (int i = 0; i < DEFAULT_BULK_THRESHOLD * 1.5; i++) { - String hash = bulkTestHash + String.valueOf(i); + String hash = randomHash(); instances.add(new EamGlobalFileInstance(notableSet2id, hash, TskData.FileKnown.BAD, null)); } @@ -1684,10 +1821,10 @@ public class CentralRepoDatamodelTest extends TestCase { // There's no way to get a count of the number of entries in the database, so just do a spot check if (DEFAULT_BULK_THRESHOLD > 10) { - String hash = bulkTestHash + "10"; + String hash = instances.stream().findFirst().get().getMD5Hash(); assertTrue("Sample bulk insert instance not found", EamDb.getInstance().isFileHashInReferenceSet(hash, notableSet2id)); } - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -1695,43 +1832,51 @@ public class CentralRepoDatamodelTest extends TestCase { // Test bulk add file instance with null list try { EamDb.getInstance().bulkInsertReferenceTypeEntries(null, fileType); - Assert.fail("bulkInsertReferenceTypeEntries failed to throw exception for null list"); + fail("bulkInsertReferenceTypeEntries failed to throw exception for null list"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test bulk add file instance with invalid reference set ID try { Set tempSet = new HashSet<>(Arrays.asList(new EamGlobalFileInstance(2345, inAllSetsHash, TskData.FileKnown.BAD, "comment"))); EamDb.getInstance().bulkInsertReferenceTypeEntries(tempSet, fileType); - Assert.fail("bulkInsertReferenceTypeEntries failed to throw exception for invalid ID"); + fail("bulkInsertReferenceTypeEntries failed to throw exception for invalid ID"); } catch (EamDbException ex) { // This is the expected behavior + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); } // Test bulk add file instance with null correlation type try { Set tempSet = new HashSet<>(Arrays.asList(new EamGlobalFileInstance(notableSet1id, inAllSetsHash, TskData.FileKnown.BAD, "comment"))); EamDb.getInstance().bulkInsertReferenceTypeEntries(tempSet, null); - Assert.fail("bulkInsertReferenceTypeEntries failed to throw exception for null type"); + fail("bulkInsertReferenceTypeEntries failed to throw exception for null type"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); } // Test getting reference instances with valid data try { List temp = EamDb.getInstance().getReferenceInstancesByTypeValue(fileType, inAllSetsHash); assertTrue("getReferenceInstancesByTypeValue returned " + temp.size() + " instances - expected 3", temp.size() == 3); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } // Test getting reference instances with non-existent data try { - List temp = EamDb.getInstance().getReferenceInstancesByTypeValue(fileType, "testHash"); + List temp = EamDb.getInstance().getReferenceInstancesByTypeValue(fileType, randomHash()); assertTrue("getReferenceInstancesByTypeValue returned " + temp.size() + " instances for non-existent value - expected 0", temp.isEmpty()); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -1739,32 +1884,40 @@ public class CentralRepoDatamodelTest extends TestCase { // Test getting reference instances an invalid type (the email table is not yet implemented) try { EamDb.getInstance().getReferenceInstancesByTypeValue(emailType, inAllSetsHash); - Assert.fail("getReferenceInstancesByTypeValue failed to throw exception for invalid table"); - } catch (EamDbException ex) { + fail("getReferenceInstancesByTypeValue failed to throw exception for invalid table"); + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting reference instances with null type try { EamDb.getInstance().getReferenceInstancesByTypeValue(null, inAllSetsHash); - Assert.fail("getReferenceInstancesByTypeValue failed to throw exception for null type"); + fail("getReferenceInstancesByTypeValue failed to throw exception for null type"); } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex){ // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting reference instances with null value try { List temp = EamDb.getInstance().getReferenceInstancesByTypeValue(fileType, null); - assertTrue("getReferenceInstancesByTypeValue returned non-empty list given null value", temp.isEmpty()); + fail("we should get an exception here"); } catch (EamDbException ex) { Exceptions.printStackTrace(ex); - Assert.fail(ex.getMessage()); + fail(ex.getMessage()); + } catch(CorrelationAttributeNormalizationException ex){ + //this is expected + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test checking existing hash/ID try { assertTrue("isFileHashInReferenceSet returned false for valid data", EamDb.getInstance().isFileHashInReferenceSet(knownHash1, knownSet1id)); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -1772,7 +1925,7 @@ public class CentralRepoDatamodelTest extends TestCase { // Test checking non-existent (but valid) hash/ID try { assertFalse("isFileHashInReferenceSet returned true for non-existent data", EamDb.getInstance().isFileHashInReferenceSet(knownHash1, notableSet1id)); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -1780,24 +1933,28 @@ public class CentralRepoDatamodelTest extends TestCase { // Test checking invalid reference set ID try { assertFalse("isFileHashInReferenceSet returned true for invalid data", EamDb.getInstance().isFileHashInReferenceSet(knownHash1, 5678)); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } // Test checking null hash try { - assertFalse("isFileHashInReferenceSet returned true for null hash", EamDb.getInstance().isFileHashInReferenceSet(null, knownSet1id)); + EamDb.getInstance().isFileHashInReferenceSet(null, knownSet1id); + fail("This should throw an exception"); } catch (EamDbException ex) { Exceptions.printStackTrace(ex); - Assert.fail(ex.getMessage()); + fail(ex.getMessage()); + } catch(CorrelationAttributeNormalizationException ex){ + //this is expected + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test checking existing hash/ID try { assertTrue("isValueInReferenceSet returned false for valid data", EamDb.getInstance().isValueInReferenceSet(knownHash1, knownSet1id, fileType.getId())); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -1806,7 +1963,7 @@ public class CentralRepoDatamodelTest extends TestCase { try { assertFalse("isValueInReferenceSet returned true for non-existent data", EamDb.getInstance().isValueInReferenceSet(knownHash1, notableSet1id, fileType.getId())); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -1815,33 +1972,40 @@ public class CentralRepoDatamodelTest extends TestCase { try { assertFalse("isValueInReferenceSet returned true for invalid data", EamDb.getInstance().isValueInReferenceSet(knownHash1, 5678, fileType.getId())); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } // Test checking null hash try { - assertFalse("isValueInReferenceSet returned true for null value", - EamDb.getInstance().isValueInReferenceSet(null, knownSet1id, fileType.getId())); + EamDb.getInstance().isValueInReferenceSet(null, knownSet1id, fileType.getId()); + fail("we should get an exception here"); } catch (EamDbException ex) { Exceptions.printStackTrace(ex); - Assert.fail(ex.getMessage()); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex){ + //this is expected + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test checking invalid type try { EamDb.getInstance().isValueInReferenceSet(knownHash1, knownSet1id, emailType.getId()); - Assert.fail("isValueInReferenceSet failed to throw exception for invalid type"); + fail("isValueInReferenceSet failed to throw exception for invalid type"); } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test known bad with notable data try { assertTrue("isArtifactKnownBadByReference returned false for notable value", EamDb.getInstance().isArtifactKnownBadByReference(fileType, notableHash1)); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -1850,7 +2014,7 @@ public class CentralRepoDatamodelTest extends TestCase { try { assertFalse("isArtifactKnownBadByReference returned true for known value", EamDb.getInstance().isArtifactKnownBadByReference(fileType, knownHash1)); - } catch (EamDbException ex) { + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -1858,35 +2022,46 @@ public class CentralRepoDatamodelTest extends TestCase { // Test known bad with non-existent data try { assertFalse("isArtifactKnownBadByReference returned true for non-existent value", - EamDb.getInstance().isArtifactKnownBadByReference(fileType, "abcdef")); - } catch (EamDbException ex) { + EamDb.getInstance().isArtifactKnownBadByReference(fileType, randomHash())); + } catch (EamDbException | CorrelationAttributeNormalizationException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } // Test known bad with null hash try { - assertFalse("isArtifactKnownBadByReference returned true for null value", - EamDb.getInstance().isArtifactKnownBadByReference(fileType, null)); + EamDb.getInstance().isArtifactKnownBadByReference(fileType, null); + fail("we should have thrown an exception"); } catch (EamDbException ex) { Exceptions.printStackTrace(ex); - Assert.fail(ex.getMessage()); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex) { + //this is expected + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test known bad with null type try { EamDb.getInstance().isArtifactKnownBadByReference(null, knownHash1); - Assert.fail("isArtifactKnownBadByReference failed to throw exception from null type"); + fail("isArtifactKnownBadByReference failed to throw exception from null type"); } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test known bad with invalid type try { - assertFalse("isArtifactKnownBadByReference returned true for invalid type", EamDb.getInstance().isArtifactKnownBadByReference(emailType, null)); + EamDb.getInstance().isArtifactKnownBadByReference(emailType, null); + fail("should get an exception here"); } catch (EamDbException ex) { Exceptions.printStackTrace(ex); - Assert.fail(ex.getMessage()); + fail(ex.getMessage()); + } catch (CorrelationAttributeNormalizationException ex) { + //this is expected + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } } @@ -1960,9 +2135,10 @@ public class CentralRepoDatamodelTest extends TestCase { try { EamGlobalSet temp = new EamGlobalSet(org1.getOrgID(), set1name, "1.0", TskData.FileKnown.BAD, false, fileType); EamDb.getInstance().newReferenceSet(temp); - Assert.fail("newReferenceSet failed to throw exception from duplicate name/version pair"); + fail("newReferenceSet failed to throw exception from duplicate name/version pair"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test creating a reference set with the same name but different version @@ -1979,43 +2155,47 @@ public class CentralRepoDatamodelTest extends TestCase { try { EamGlobalSet temp = new EamGlobalSet(5000, "tempName", "", TskData.FileKnown.BAD, false, fileType); EamDb.getInstance().newReferenceSet(temp); - Assert.fail("newReferenceSet failed to throw exception from invalid org ID"); + fail("newReferenceSet failed to throw exception from invalid org ID"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test creating a reference set with null name try { EamGlobalSet temp = new EamGlobalSet(org2.getOrgID(), null, "", TskData.FileKnown.BAD, false, fileType); EamDb.getInstance().newReferenceSet(temp); - Assert.fail("newReferenceSet failed to throw exception from null name"); + fail("newReferenceSet failed to throw exception from null name"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test creating a reference set with null version try { EamGlobalSet temp = new EamGlobalSet(org2.getOrgID(), "tempName", null, TskData.FileKnown.BAD, false, fileType); EamDb.getInstance().newReferenceSet(temp); - Assert.fail("newReferenceSet failed to throw exception from null version"); + fail("newReferenceSet failed to throw exception from null version"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test creating a reference set with null file known status try { EamGlobalSet temp = new EamGlobalSet(org2.getOrgID(), "tempName", "", null, false, fileType); EamDb.getInstance().newReferenceSet(temp); - Assert.fail("newReferenceSet failed to throw exception from null file known status"); + fail("newReferenceSet failed to throw exception from null file known status"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test creating a reference set with null file type try { EamGlobalSet temp = new EamGlobalSet(org2.getOrgID(), "tempName", "", TskData.FileKnown.BAD, false, null); EamDb.getInstance().newReferenceSet(temp); - Assert.fail("newReferenceSet failed to throw exception from null file type"); + fail("newReferenceSet failed to throw exception from null file type"); } catch (EamDbException ex) { // This is the expected behavior } @@ -2125,9 +2305,10 @@ public class CentralRepoDatamodelTest extends TestCase { // Test null argument to getAllReferenceSets try { EamDb.getInstance().getAllReferenceSets(null); - Assert.fail("getAllReferenceSets failed to throw exception from null type argument"); + fail("getAllReferenceSets failed to throw exception from null type argument"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test deleting an existing reference set @@ -2172,9 +2353,10 @@ public class CentralRepoDatamodelTest extends TestCase { // Test getting reference set organization for non-existent reference set try { EamDb.getInstance().getReferenceSetOrganization(4567); - Assert.fail("getReferenceSetOrganization failed to throw exception for invalid reference set ID"); + fail("getReferenceSetOrganization failed to throw exception for invalid reference set ID"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } } @@ -2217,7 +2399,7 @@ public class CentralRepoDatamodelTest extends TestCase { try { CorrelationDataSource temp = new CorrelationDataSource(case2, dataSourceAid, dataSourceAname); EamDb.getInstance().newDataSource(temp); - Assert.fail("newDataSource did not throw exception from duplicate data source"); + fail("newDataSource did not throw exception from duplicate data source"); } catch (EamDbException ex) { // This is the expected behavior } @@ -2237,27 +2419,30 @@ public class CentralRepoDatamodelTest extends TestCase { CorrelationCase correlationCase = new CorrelationCase("1", "test"); CorrelationDataSource temp = new CorrelationDataSource(correlationCase, "tempID", "tempName"); EamDb.getInstance().newDataSource(temp); - Assert.fail("newDataSource did not throw exception from invalid case ID"); + fail("newDataSource did not throw exception from invalid case ID"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test creating a data source with null device ID try { CorrelationDataSource temp = new CorrelationDataSource(case2, null, "tempName"); EamDb.getInstance().newDataSource(temp); - Assert.fail("newDataSource did not throw exception from null device ID"); + fail("newDataSource did not throw exception from null device ID"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test creating a data source with null name try { CorrelationDataSource temp = new CorrelationDataSource(case2, "tempID", null); EamDb.getInstance().newDataSource(temp); - Assert.fail("newDataSource did not throw exception from null name"); + fail("newDataSource did not throw exception from null name"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting a data source with valid case and ID @@ -2281,9 +2466,10 @@ public class CentralRepoDatamodelTest extends TestCase { // Test getting a data source with a null case try { EamDb.getInstance().getDataSource(null, dataSourceAid); - Assert.fail("getDataSource did not throw exception from null case"); + fail("getDataSource did not throw exception from null case"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting a data source with null ID @@ -2378,18 +2564,20 @@ public class CentralRepoDatamodelTest extends TestCase { try { CorrelationCase tempCase = new CorrelationCase(null, "nullUuidCase"); EamDb.getInstance().newCase(tempCase); - Assert.fail("newCase did not throw expected exception from null uuid"); + fail("newCase did not throw expected exception from null uuid"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test null name try { CorrelationCase tempCase = new CorrelationCase("nullCaseUuid", null); EamDb.getInstance().newCase(tempCase); - Assert.fail("newCase did not throw expected exception from null name"); + fail("newCase did not throw expected exception from null name"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test creating a case with an already used UUID @@ -2421,9 +2609,10 @@ public class CentralRepoDatamodelTest extends TestCase { try { Case nullCase = null; EamDb.getInstance().newCase(nullCase); - Assert.fail("newCase did not throw expected exception from null case"); + fail("newCase did not throw expected exception from null case"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test update case @@ -2471,9 +2660,10 @@ public class CentralRepoDatamodelTest extends TestCase { // Test update case with null case try { EamDb.getInstance().updateCase(null); - Assert.fail("updateCase did not throw expected exception from null case"); + fail("updateCase did not throw expected exception from null case"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Test getting a case from an Autopsy case @@ -2554,9 +2744,10 @@ public class CentralRepoDatamodelTest extends TestCase { // Test bulk case insert with null list try { EamDb.getInstance().bulkInsertCases(null); - Assert.fail("bulkInsertCases did not throw expected exception from null list"); + fail("bulkInsertCases did not throw expected exception from null list"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } } finally { try { @@ -2608,7 +2799,7 @@ public class CentralRepoDatamodelTest extends TestCase { // Test null name try { EamDb.getInstance().newDbInfo(null, value1); - Assert.fail("newDbInfo did not throw expected exception from null name"); + fail("newDbInfo did not throw expected exception from null name"); } catch (EamDbException ex) { // This is the expected behavior } @@ -2616,9 +2807,10 @@ public class CentralRepoDatamodelTest extends TestCase { // Test null value try { EamDb.getInstance().newDbInfo(name2, null); - Assert.fail("newDbInfo did not throw expected exception from null value"); + fail("newDbInfo did not throw expected exception from null value"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Try getting the dbInfo entry that should exist @@ -2660,9 +2852,10 @@ public class CentralRepoDatamodelTest extends TestCase { // Try updating an existing value to null try { EamDb.getInstance().updateDbInfo(name1, null); - Assert.fail("updateDbInfo did not throw expected exception from null value"); + fail("updateDbInfo did not throw expected exception from null value"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } // Try updating a null name @@ -2677,11 +2870,33 @@ public class CentralRepoDatamodelTest extends TestCase { // Try updating the value for a non-existant name try { EamDb.getInstance().updateDbInfo(name1, null); - Assert.fail("updateDbInfo did not throw expected exception from non-existent name"); + fail("updateDbInfo did not throw expected exception from non-existent name"); } catch (EamDbException ex) { // This is the expected behavior + assertTrue(THIS_IS_THE_EXPECTED_BEHAVIOR, true); } } + private static final String THIS_IS_THE_EXPECTED_BEHAVIOR = "This is the expected behavior."; + + private static String randomHash() { + + String[] chars = {"a", "b", "c", "d", "e", "f", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}; + + Random random = new Random(); + IntStream ints = random.ints(32, 0, chars.length - 1); + + Iterator it = ints.iterator(); + + StringBuilder md5 = new StringBuilder(32); + + while(it.hasNext()){ + Integer i = it.next(); + String character = chars[i]; + md5.append(character); + } + + return md5.toString(); + } public class AttributeInstanceTableCallback implements InstanceTableCallback { @@ -2710,7 +2925,5 @@ public class CentralRepoDatamodelTest extends TestCase { public int getCounterNamingConvention(){ return counterNamingConvention; } - } - -} +} \ No newline at end of file diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizerTest.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizerTest.java new file mode 100644 index 0000000000..76d28e7ea6 --- /dev/null +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizerTest.java @@ -0,0 +1,315 @@ +/* + * + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.centralrepository.datamodel; + +import junit.framework.Test; +import org.junit.Assert; +import org.netbeans.junit.NbModuleSuite; +import org.netbeans.junit.NbTestCase; +import org.openide.util.Exceptions; + +/** + * Tests for validation on each correlation attribute type. + */ +public class CorrelationAttributeNormalizerTest extends NbTestCase { + + public static Test suite() { + NbModuleSuite.Configuration conf = NbModuleSuite.createConfiguration(CorrelationAttributeNormalizerTest.class). + clusters(".*"). + enableModules(".*"); + return conf.suite(); + } + + public CorrelationAttributeNormalizerTest(String name) { + super(name); + } + + public void testValidateMd5() { + final String aValidHash = "e34a8899ef6468b74f8a1048419ccc8b"; //should pass + final String anInValidHash = "e34asdfa8899ef6468b74f8a1048419ccc8b"; //should fail + final String aValidHashWithCaps = "E34A8899EF6468B74F8A1048419CCC8B"; //should pass and be lowered + final String emptyHash = ""; //should fail + final String nullHash = null; //should fail + + final int FILES_TYPE_ID = CorrelationAttributeInstance.FILES_TYPE_ID; + + try { + assertTrue("This hash should just work", CorrelationAttributeNormalizer.normalize(FILES_TYPE_ID, aValidHash).equals(aValidHash)); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } + try { + assertTrue("This hash just needs to be converted to lower case", CorrelationAttributeNormalizer.normalize(CorrelationAttributeInstance.FILES_TYPE_ID, aValidHashWithCaps).equals(aValidHash)); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } + try { + CorrelationAttributeNormalizer.normalize(FILES_TYPE_ID, anInValidHash); + fail(THIS_SHOULD_HAVE_THROWN_AN_EXCEPTION); + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + try { + CorrelationAttributeNormalizer.normalize(FILES_TYPE_ID, emptyHash); + fail(THIS_SHOULD_HAVE_THROWN_AN_EXCEPTION); + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + try { + CorrelationAttributeNormalizer.normalize(FILES_TYPE_ID, nullHash); + fail(THIS_SHOULD_HAVE_THROWN_AN_EXCEPTION); + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + } + private static final String WE_EXPECT_AN_EXCEPTION_HERE = "We expect an exception here."; + private static final String THIS_SHOULD_HAVE_THROWN_AN_EXCEPTION = "This should have thrown an exception."; + + public void testValidateDomain() { + final String goodDomainOne = "www.test.com"; //should pass + final String badDomainTwo = "http://www.test.com"; //should fail (includes protocol) + final String goodDomainThree = "test.com"; //should pass + final String badDomainFour = "http://1270.0.1"; //should fail + final String badDomainFive = "?>\\/)(*&.com"; //should fail + final String badDomainSix = null; //should fail + final String badDomainSeven = ""; //should fail + final String badDomainEight = "HTTP://tests.com"; //should fail + final String badDomainNine = "http://www.test.com/aPage?aQuestion=aParam&anotherQuestion=anotherParam"; //should fail + final String goodDomainTen = "WWW.TEST.COM"; //should pass but be lowered + final String goodDomainEleven = "TEST.COM"; //should pass but be lowered + + final int DOMAIN_TYPE_ID = CorrelationAttributeInstance.DOMAIN_TYPE_ID; + + try { + assertTrue(THIS_DOMAIN_SHOULD_PASS, CorrelationAttributeNormalizer.normalize(DOMAIN_TYPE_ID, goodDomainOne).equals(goodDomainOne)); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } + try { + CorrelationAttributeNormalizer.normalize(DOMAIN_TYPE_ID, badDomainTwo); + fail(THIS_SHOULD_HAVE_THROWN_AN_EXCEPTION); + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + try { + assertTrue(THIS_DOMAIN_SHOULD_PASS, CorrelationAttributeNormalizer.normalize(DOMAIN_TYPE_ID, goodDomainThree).equals(goodDomainThree)); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } + try { + assertTrue(THIS_DOMAIN_SHOULD_PASS, CorrelationAttributeNormalizer.normalize(DOMAIN_TYPE_ID, badDomainFour).equals(badDomainFour)); + fail(THIS_SHOULD_HAVE_THROWN_AN_EXCEPTION); + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + try { + CorrelationAttributeNormalizer.normalize(DOMAIN_TYPE_ID, badDomainFive); + fail(THIS_SHOULD_HAVE_THROWN_AN_EXCEPTION); + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + try { + CorrelationAttributeNormalizer.normalize(DOMAIN_TYPE_ID, badDomainSix); + fail(THIS_SHOULD_HAVE_THROWN_AN_EXCEPTION); + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + try { + CorrelationAttributeNormalizer.normalize(DOMAIN_TYPE_ID, badDomainSeven); + fail(THIS_SHOULD_HAVE_THROWN_AN_EXCEPTION); + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + try { + CorrelationAttributeNormalizer.normalize(DOMAIN_TYPE_ID, badDomainEight); + fail("This should have thrown an exception"); + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + try { + CorrelationAttributeNormalizer.normalize(DOMAIN_TYPE_ID, badDomainNine); + fail("This should have thrown an exception"); + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + try { + assertTrue(THIS_DOMAIN_SHOULD_PASS, CorrelationAttributeNormalizer.normalize(DOMAIN_TYPE_ID, goodDomainTen).equals(goodDomainTen.toLowerCase())); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } + try { + assertTrue(THIS_DOMAIN_SHOULD_PASS, CorrelationAttributeNormalizer.normalize(DOMAIN_TYPE_ID, goodDomainEleven).equals(goodDomainEleven.toLowerCase())); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } + } + private static final String THIS_DOMAIN_SHOULD_PASS = "This domain should pass."; + + public void testValidateEmail() { + final String goodEmailOne = "bsweeney@cipehrtechsolutions.com"; //should pass + final String goodEmailTwo = "BSWEENEY@ciphertechsolutions.com"; //should pass and be lowered + final String badEmailThree = ""; //should fail + final String badEmailFour = null; //should fail + final String badEmailFive = "asdf"; //should fail + final String goodEmailSix = "asdf@asdf"; //TODO looks bad but the lib accepts it... + final String badEmailSeven = "asdf.asdf"; //should + + final int EMAIL_TYPE_ID = CorrelationAttributeInstance.EMAIL_TYPE_ID; + + try { + assertTrue("This email should pass.", CorrelationAttributeNormalizer.normalize(EMAIL_TYPE_ID, goodEmailOne).equals(goodEmailOne)); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } + try { + assertTrue("This email should pass.", CorrelationAttributeNormalizer.normalize(EMAIL_TYPE_ID, goodEmailTwo).equals(goodEmailTwo.toLowerCase())); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } + try { + CorrelationAttributeNormalizer.normalize(EMAIL_TYPE_ID, badEmailThree); + fail(THIS_SHOULD_HAVE_THROWN_AN_EXCEPTION); + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + try { + CorrelationAttributeNormalizer.normalize(EMAIL_TYPE_ID, badEmailFour); + fail(THIS_SHOULD_HAVE_THROWN_AN_EXCEPTION); + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + try { + CorrelationAttributeNormalizer.normalize(EMAIL_TYPE_ID, badEmailFive); + fail(THIS_SHOULD_HAVE_THROWN_AN_EXCEPTION); + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + try { //TODO consider a better library? + assertTrue("This email should pass", CorrelationAttributeNormalizer.normalize(EMAIL_TYPE_ID, goodEmailSix).equals(goodEmailSix)); + } catch (CorrelationAttributeNormalizationException ex) { + fail(ex.getMessage()); + } + try { + CorrelationAttributeNormalizer.normalize(EMAIL_TYPE_ID, badEmailSeven); + fail(THIS_SHOULD_HAVE_THROWN_AN_EXCEPTION); + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + } + + public void testValidatePhone() { + final String goodPnOne = "19784740486"; + final String goodPnTwo = "1(978) 474-0486"; + final String goodPnThree = "+19784740486"; + final String goodPnFour = "1 978-474-0486"; + final String badPnFive = "9879879819784740486"; + final String goodPnSix = "+1(978) 474-0486"; + final String goodPnSeven = "+1(978) 474-0486"; + final String badPnEight = "asdfasdfasdf"; + final String badPnNine = "asdf19784740486adsf"; + + final int PHONE_TYPE_ID = CorrelationAttributeInstance.PHONE_TYPE_ID; + + try { + assertTrue(THIS_PHONE_NUMBER_SHOULD_PASS, CorrelationAttributeNormalizer.normalize(PHONE_TYPE_ID, goodPnOne).equals(goodPnOne)); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } + try { + assertTrue(THIS_PHONE_NUMBER_SHOULD_PASS, CorrelationAttributeNormalizer.normalize(PHONE_TYPE_ID, goodPnTwo).equals(goodPnOne)); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } + try { + assertTrue(THIS_PHONE_NUMBER_SHOULD_PASS, CorrelationAttributeNormalizer.normalize(PHONE_TYPE_ID, goodPnThree).equals(goodPnThree)); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } + try { + assertTrue(THIS_PHONE_NUMBER_SHOULD_PASS, CorrelationAttributeNormalizer.normalize(PHONE_TYPE_ID, goodPnFour).equals(goodPnOne)); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } + try { + CorrelationAttributeNormalizer.normalize(PHONE_TYPE_ID, badPnFive); + //fail("This should have thrown an exception."); //this will eventually pass when we do a better job at this + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + try { + assertTrue(THIS_PHONE_NUMBER_SHOULD_PASS, CorrelationAttributeNormalizer.normalize(PHONE_TYPE_ID, goodPnSix).equals(goodPnThree)); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } + try { + assertTrue(THIS_PHONE_NUMBER_SHOULD_PASS, CorrelationAttributeNormalizer.normalize(PHONE_TYPE_ID, goodPnSeven).equals(goodPnThree)); + } catch (CorrelationAttributeNormalizationException ex) { + Exceptions.printStackTrace(ex); + fail(ex.getMessage()); + } + try { + CorrelationAttributeNormalizer.normalize(PHONE_TYPE_ID, badPnEight); + fail("This should have thrown an exception."); + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + try { + CorrelationAttributeNormalizer.normalize(PHONE_TYPE_ID, badPnNine); + fail("This should have thrown an exception."); + } catch (CorrelationAttributeNormalizationException ex) { + assertTrue(WE_EXPECT_AN_EXCEPTION_HERE, true); + } + } + private static final String THIS_PHONE_NUMBER_SHOULD_PASS = "This phone number should pass."; + + public void testValidateUsbId() { + //TODO will need to be updated once usb validation does somethign interesting + final String goodIdOne = "0202:AAFF"; //should pass + /*final String goodIdTwo = "0202:aaff"; //should pass + final String badIdThree = "0202:axxf"; //should fail + final String badIdFour = ""; //should fail + final String badIdFive = null; //should fail + final String goodIdSix = "0202 AAFF"; //should pass + final String goodIdSeven = "0202AAFF"; //should pass + final String goodIdEight = "0202-AAFF"; //should pass*/ + + final int USBID_TYPE_ID = CorrelationAttributeInstance.USBID_TYPE_ID; + + try { + assertTrue(THIS_USB_ID_SHOULD_PASS, CorrelationAttributeNormalizer.normalize(USBID_TYPE_ID, goodIdOne).equals(goodIdOne)); + } catch (CorrelationAttributeNormalizationException ex) { + Assert.fail(ex.getMessage()); + } + } + private static final String THIS_USB_ID_SHOULD_PASS = "This USB ID should pass."; +} diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/CommonAttributeSearchInterCaseTests.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/CommonAttributeSearchInterCaseTests.java new file mode 100644 index 0000000000..966d0b8bce --- /dev/null +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/CommonAttributeSearchInterCaseTests.java @@ -0,0 +1,174 @@ +/* + * + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.commonfilessearch; + +import java.nio.file.Path; +import java.sql.SQLException; +import java.util.Map; +import junit.framework.Assert; +import junit.framework.Test; +import org.netbeans.junit.NbModuleSuite; +import org.netbeans.junit.NbTestCase; +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; +import org.sleuthkit.autopsy.commonfilesearch.AbstractCommonAttributeSearcher; +import org.sleuthkit.autopsy.commonfilesearch.AllInterCaseCommonAttributeSearcher; +import org.sleuthkit.autopsy.commonfilesearch.CommonAttributeSearchResults; +import static org.sleuthkit.autopsy.commonfilessearch.InterCaseTestUtils.CASE1; +import static org.sleuthkit.autopsy.commonfilessearch.InterCaseTestUtils.CASE2; +import static org.sleuthkit.autopsy.commonfilessearch.InterCaseTestUtils.CASE3; +import static org.sleuthkit.autopsy.commonfilessearch.InterCaseTestUtils.CASE4; +import static org.sleuthkit.autopsy.commonfilessearch.InterCaseTestUtils.verifyInstanceCount; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Search for commonality in different sorts of attributes (files, usb devices, + * emails, domains). Observe that frequency filtering works for various types. + * + * TODO (JIRA-4166): The following tests are commented out because the + * functional test framework needs to be able to configure the keyword search + * ingest module to produce instances of the correlation attributes for the + * tests. This cannot be easily done at present because the keyword search + * module resides in an NBM with a dependency on the Autopsy-Core NBM; the + * otherwise obvious solution of publicly exposing the keyword search module + * settings fails due to a circular dependency. + * + */ +public class CommonAttributeSearchInterCaseTests extends NbTestCase { + + private final InterCaseTestUtils utils; + + public static Test suite() { + NbModuleSuite.Configuration conf = NbModuleSuite.createConfiguration(CommonAttributeSearchInterCaseTests.class). + clusters(".*"). + enableModules(".*"); + return conf.suite(); + } + + public CommonAttributeSearchInterCaseTests(String name) { + super(name); + this.utils = new InterCaseTestUtils(this); + } + + @Override + public void setUp() { + this.utils.clearTestDir(); + try { + this.utils.enableCentralRepo(); + + String[] cases = new String[]{ + CASE1, + CASE2, + CASE3, + CASE4}; + + Path[][] paths = { + {this.utils.attrCase1Path}, + {this.utils.attrCase2Path}, + {this.utils.attrCase3Path}, + {this.utils.attrCase4Path}}; + + this.utils.createCases(cases, paths, this.utils.getIngestSettingsForKitchenSink(), InterCaseTestUtils.CASE1); + } catch (TskCoreException | EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex.getMessage()); + } + } + + @Override + public void tearDown() { + this.utils.clearTestDir(); + this.utils.tearDown(); + } + + /** + * Run a search on the given type and ensure that all results are off that + * type. + * + * No frequency filtering applied. + * + * @param type + */ + private void assertResultsAreOfType(CorrelationAttributeInstance.Type type) { + + try { + Map dataSources = this.utils.getDataSourceMap(); + + AbstractCommonAttributeSearcher builder = new AllInterCaseCommonAttributeSearcher(dataSources, false, false, type, 0); + + CommonAttributeSearchResults metadata = builder.findMatches(); + + metadata.size(); + + assertFalse(verifyInstanceCount(metadata, 0)); + + assertTrue(this.utils.areAllResultsOfType(metadata, type)); + + } catch (TskCoreException | NoCurrentCaseException | SQLException | EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex.getMessage()); + } + } + + /** + * Test that a search for each type returns results of that type only. + */ + public void testOne() { +// assertResultsAreOfType(this.utils.USB_ID_TYPE); +// assertResultsAreOfType(this.utils.DOMAIN_TYPE); +// assertResultsAreOfType(this.utils.FILE_TYPE); +// assertResultsAreOfType(this.utils.EMAIL_TYPE); +// assertResultsAreOfType(this.utils.PHONE_TYPE); + } + + /** + * Test that the frequency filter behaves reasonably for attributes other + * than the file type. + */ + public void testTwo() { + try { + Map dataSources = this.utils.getDataSourceMap(); + + AbstractCommonAttributeSearcher builder; + CommonAttributeSearchResults metadata; + + builder = new AllInterCaseCommonAttributeSearcher(dataSources, false, false, this.utils.USB_ID_TYPE, 100); + metadata = builder.findMatches(); + metadata.size(); + //assertTrue("This should yield 13 results.", verifyInstanceCount(metadata, 13)); + + builder = new AllInterCaseCommonAttributeSearcher(dataSources, false, false, this.utils.USB_ID_TYPE, 20); + metadata = builder.findMatches(); + metadata.size(); + //assertTrue("This should yield no results.", verifyInstanceCount(metadata, 0)); + + builder = new AllInterCaseCommonAttributeSearcher(dataSources, false, false, this.utils.USB_ID_TYPE, 90); + metadata = builder.findMatches(); + metadata.size(); + //assertTrue("This should yield 2 results.", verifyInstanceCount(metadata, 2)); + + } catch (TskCoreException | NoCurrentCaseException | SQLException | EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex.getMessage()); + } + } +} diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/IngestedWithHashAndFileTypeInterCaseTests.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/IngestedWithHashAndFileTypeInterCaseTests.java index a425ecbcc8..8f4f96e7a3 100644 --- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/IngestedWithHashAndFileTypeInterCaseTests.java +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/IngestedWithHashAndFileTypeInterCaseTests.java @@ -19,6 +19,7 @@ */ package org.sleuthkit.autopsy.commonfilessearch; +import java.nio.file.Path; import java.sql.SQLException; import java.util.Map; import junit.framework.Test; @@ -27,6 +28,7 @@ import org.netbeans.junit.NbTestCase; import org.openide.util.Exceptions; import junit.framework.Assert; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; import org.sleuthkit.autopsy.commonfilesearch.AbstractCommonAttributeSearcher; import org.sleuthkit.autopsy.commonfilesearch.AllInterCaseCommonAttributeSearcher; @@ -37,7 +39,7 @@ import org.sleuthkit.datamodel.TskCoreException; /** * Tests with case 3 as the current case. - * + * * If I use the search all cases option: One node for Hash A (1_1_A.jpg, * 1_2_A.jpg, 3_1_A.jpg) If I search for matches only in Case 1: One node for * Hash A (1_1_A.jpg, 1_2_A.jpg, 3_1_A.jpg) If I search for matches only in Case @@ -45,27 +47,38 @@ import org.sleuthkit.datamodel.TskCoreException; * all data sources: One node for Hash C (3_1_C.jpg, 3_2_C.jpg) */ public class IngestedWithHashAndFileTypeInterCaseTests extends NbTestCase { - + private final InterCaseTestUtils utils; - + public static Test suite() { NbModuleSuite.Configuration conf = NbModuleSuite.createConfiguration(IngestedWithHashAndFileTypeInterCaseTests.class). clusters(".*"). enableModules(".*"); return conf.suite(); } - + public IngestedWithHashAndFileTypeInterCaseTests(String name) { super(name); this.utils = new InterCaseTestUtils(this); } - + @Override - public void setUp(){ + public void setUp() { this.utils.clearTestDir(); try { this.utils.enableCentralRepo(); - this.utils.createCases(this.utils.getIngestSettingsForHashAndFileType(), InterCaseTestUtils.CASE3); + + String[] cases = new String[]{ + CASE1, + CASE2, + CASE3}; + + Path[][] paths = { + {this.utils.case1DataSet1Path, this.utils.case1DataSet2Path}, + {this.utils.case2DataSet1Path, this.utils.case2DataSet2Path}, + {this.utils.case3DataSet1Path, this.utils.case3DataSet2Path}}; + + this.utils.createCases(cases, paths, this.utils.getIngestSettingsForHashAndFileType(), InterCaseTestUtils.CASE3); } catch (TskCoreException | EamDbException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); @@ -73,118 +86,115 @@ public class IngestedWithHashAndFileTypeInterCaseTests extends NbTestCase { } @Override - public void tearDown(){ + public void tearDown() { this.utils.clearTestDir(); this.utils.tearDown(); } - + /** * Search All cases with no file type filtering. */ public void testOne() { try { Map dataSources = this.utils.getDataSourceMap(); - + //note that the params false and false are presently meaningless because that feature is not supported yet - AbstractCommonAttributeSearcher builder = new AllInterCaseCommonAttributeSearcher(dataSources, false, false, 0); - - CommonAttributeSearchResults metadata = builder.findFiles(); - + AbstractCommonAttributeSearcher builder = new AllInterCaseCommonAttributeSearcher(dataSources, false, false, this.utils.FILE_TYPE, 0); + CommonAttributeSearchResults metadata = builder.findMatches(); + assertTrue("Results should not be empty", metadata.size() != 0); - + //case 1 data set 1 assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_0_DAT, CASE1_DATASET_1, CASE1, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_PDF, CASE1_DATASET_1, CASE1, 1)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_JPG, CASE1_DATASET_1, CASE1, 1)); - + //case 1 data set 2 assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_0_DAT, CASE1_DATASET_2, CASE1, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_PDF, CASE1_DATASET_2, CASE1, 1)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_JPG, CASE1_DATASET_2, CASE1, 1)); - + //case 2 data set 1 assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_B_PDF, CASE2_DATASET_1, CASE2, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_B_JPG, CASE2_DATASET_1, CASE2, 0)); - + //case 2 data set 2 assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_PDF, CASE2_DATASET_2, CASE2, 1)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_JPG, CASE2_DATASET_2, CASE2, 1)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_D_DOC, CASE2_DATASET_2, CASE2, 1)); - + //case 3 data set 1 assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_JPG, CASE3_DATASET_1, CASE3, 1)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_PDF, CASE3_DATASET_1, CASE3, 1)); - assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_C_JPG, CASE3_DATASET_1, CASE3, 0)); + assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_C_JPG, CASE3_DATASET_1, CASE3, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_C_PDF, CASE3_DATASET_1, CASE3, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_D_JPG, CASE3_DATASET_1, CASE3, 0)); - + //case 3 data set 2 assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_C_JPG, CASE3_DATASET_2, CASE3, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_C_PDF, CASE3_DATASET_2, CASE3, 0)); - assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_D_DOC, CASE3_DATASET_2, CASE3, 1)); - - + assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_D_DOC, CASE3_DATASET_2, CASE3, 1)); + } catch (TskCoreException | NoCurrentCaseException | SQLException | EamDbException ex) { - Exceptions.printStackTrace(ex); + Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } } - + /** * Search All cases with no file type filtering. */ public void testTwo() { try { Map dataSources = this.utils.getDataSourceMap(); - + int matchesMustAlsoBeFoundInThisCase = this.utils.getCaseMap().get(CASE2); - - AbstractCommonAttributeSearcher builder = new SingleInterCaseCommonAttributeSearcher(matchesMustAlsoBeFoundInThisCase, dataSources, false, false, 0); - - CommonAttributeSearchResults metadata = builder.findFiles(); + CorrelationAttributeInstance.Type fileType = CorrelationAttributeInstance.getDefaultCorrelationTypes().get(0); + AbstractCommonAttributeSearcher builder = new SingleInterCaseCommonAttributeSearcher(matchesMustAlsoBeFoundInThisCase, dataSources, false, false, fileType, 0); + CommonAttributeSearchResults metadata = builder.findMatches(); + assertTrue("Results should not be empty", metadata.size() != 0); - + //case 1 data set 1 assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_0_DAT, CASE1_DATASET_1, CASE1, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_PDF, CASE1_DATASET_1, CASE1, 1)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_JPG, CASE1_DATASET_1, CASE1, 1)); - + //case 1 data set 2 assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_0_DAT, CASE1_DATASET_2, CASE1, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_PDF, CASE1_DATASET_2, CASE1, 1)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_JPG, CASE1_DATASET_2, CASE1, 1)); - + //case 2 data set 1 assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_B_PDF, CASE2_DATASET_1, CASE2, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_B_JPG, CASE2_DATASET_1, CASE2, 0)); - + //case 2 data set 2 assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_PDF, CASE2_DATASET_2, CASE2, 1)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_JPG, CASE2_DATASET_2, CASE2, 1)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_D_DOC, CASE2_DATASET_2, CASE2, 1)); - + //case 3 data set 1 assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_JPG, CASE3_DATASET_1, CASE3, 1)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_PDF, CASE3_DATASET_1, CASE3, 1)); - assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_C_JPG, CASE3_DATASET_1, CASE3, 0)); + assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_C_JPG, CASE3_DATASET_1, CASE3, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_C_PDF, CASE3_DATASET_1, CASE3, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_D_JPG, CASE3_DATASET_1, CASE3, 0)); - + //case 3 data set 2 assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_C_JPG, CASE3_DATASET_2, CASE3, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_C_PDF, CASE3_DATASET_2, CASE3, 0)); - assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_D_DOC, CASE3_DATASET_2, CASE3, 1)); - - + assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_D_DOC, CASE3_DATASET_2, CASE3, 1)); + } catch (TskCoreException | NoCurrentCaseException | SQLException | EamDbException ex) { - Exceptions.printStackTrace(ex); + Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } } /** - * We should be able to observe that certain files o no longer returned + * We should be able to observe that certain files are no longer returned * in the result set since they do not appear frequently enough. */ public void testThree(){ @@ -192,34 +202,35 @@ public class IngestedWithHashAndFileTypeInterCaseTests extends NbTestCase { Map dataSources = this.utils.getDataSourceMap(); //note that the params false and false are presently meaningless because that feature is not supported yet - AbstractCommonAttributeSearcher builder = new AllInterCaseCommonAttributeSearcher(dataSources, false, false, 50); + CorrelationAttributeInstance.Type fileType = CorrelationAttributeInstance.getDefaultCorrelationTypes().get(0); + AbstractCommonAttributeSearcher builder = new AllInterCaseCommonAttributeSearcher(dataSources, false, false, fileType, 50); - CommonAttributeSearchResults metadata = builder.findFiles(); + CommonAttributeSearchResults metadata = builder.findMatches(); assertTrue("Results should not be empty", metadata.size() != 0); //case 1 data set 1 assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_0_DAT, CASE1_DATASET_1, CASE1, 0)); - assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_PDF, CASE1_DATASET_1, CASE1, 1)); - assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_JPG, CASE1_DATASET_1, CASE1, 1)); + assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_PDF, CASE1_DATASET_1, CASE1, 0)); + assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_JPG, CASE1_DATASET_1, CASE1, 0)); //case 1 data set 2 assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_0_DAT, CASE1_DATASET_2, CASE1, 0)); - assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_PDF, CASE1_DATASET_2, CASE1, 1)); - assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_JPG, CASE1_DATASET_2, CASE1, 1)); + assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_PDF, CASE1_DATASET_2, CASE1, 0)); + assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_JPG, CASE1_DATASET_2, CASE1, 0)); //case 2 data set 1 assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_B_PDF, CASE2_DATASET_1, CASE2, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_B_JPG, CASE2_DATASET_1, CASE2, 0)); //case 2 data set 2 - assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_PDF, CASE2_DATASET_2, CASE2, 1)); - assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_JPG, CASE2_DATASET_2, CASE2, 1)); - assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_D_DOC, CASE2_DATASET_2, CASE2, 0)); + assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_PDF, CASE2_DATASET_2, CASE2, 0)); + assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_JPG, CASE2_DATASET_2, CASE2, 0)); + assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_D_DOC, CASE2_DATASET_2, CASE2, 1)); //case 3 data set 1 - assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_JPG, CASE3_DATASET_1, CASE3, 1)); - assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_PDF, CASE3_DATASET_1, CASE3, 1)); + assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_JPG, CASE3_DATASET_1, CASE3, 0)); + assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_A_PDF, CASE3_DATASET_1, CASE3, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_C_JPG, CASE3_DATASET_1, CASE3, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_C_PDF, CASE3_DATASET_1, CASE3, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_D_JPG, CASE3_DATASET_1, CASE3, 0)); @@ -227,7 +238,7 @@ public class IngestedWithHashAndFileTypeInterCaseTests extends NbTestCase { //case 3 data set 2 assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_C_JPG, CASE3_DATASET_2, CASE3, 0)); assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_C_PDF, CASE3_DATASET_2, CASE3, 0)); - assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_D_DOC, CASE3_DATASET_2, CASE3, 0)); + assertTrue(verifyInstanceExistanceAndCount(metadata, HASH_D_DOC, CASE3_DATASET_2, CASE3, 1)); } catch (TskCoreException | NoCurrentCaseException | SQLException | EamDbException ex) { Exceptions.printStackTrace(ex); diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/IngestedWithHashAndFileTypeIntraCaseTests.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/IngestedWithHashAndFileTypeIntraCaseTests.java index f717e8a670..4a423c800e 100644 --- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/IngestedWithHashAndFileTypeIntraCaseTests.java +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/IngestedWithHashAndFileTypeIntraCaseTests.java @@ -100,7 +100,7 @@ public class IngestedWithHashAndFileTypeIntraCaseTests extends NbTestCase { Map dataSources = this.utils.getDataSourceMap(); AbstractCommonAttributeSearcher allSourcesBuilder = new AllIntraCaseCommonAttributeSearcher(dataSources, false, false, 0); - CommonAttributeSearchResults metadata = allSourcesBuilder.findFiles(); + CommonAttributeSearchResults metadata = allSourcesBuilder.findMatches(); Map objectIdToDataSource = IntraCaseTestUtils.mapFileInstancesToDataSources(metadata); @@ -141,7 +141,7 @@ public class IngestedWithHashAndFileTypeIntraCaseTests extends NbTestCase { Map dataSources = this.utils.getDataSourceMap(); AbstractCommonAttributeSearcher allSourcesBuilder = new AllIntraCaseCommonAttributeSearcher(dataSources, true, false, 0); - CommonAttributeSearchResults metadata = allSourcesBuilder.findFiles(); + CommonAttributeSearchResults metadata = allSourcesBuilder.findMatches(); Map objectIdToDataSource = mapFileInstancesToDataSources(metadata); @@ -182,7 +182,7 @@ public class IngestedWithHashAndFileTypeIntraCaseTests extends NbTestCase { Map dataSources = this.utils.getDataSourceMap(); AbstractCommonAttributeSearcher allSourcesBuilder = new AllIntraCaseCommonAttributeSearcher(dataSources, false, true, 0); - CommonAttributeSearchResults metadata = allSourcesBuilder.findFiles(); + CommonAttributeSearchResults metadata = allSourcesBuilder.findMatches(); Map objectIdToDataSource = mapFileInstancesToDataSources(metadata); @@ -224,7 +224,7 @@ public class IngestedWithHashAndFileTypeIntraCaseTests extends NbTestCase { Long first = getDataSourceIdByName(SET1, dataSources); AbstractCommonAttributeSearcher singleSourceBuilder = new SingleIntraCaseCommonAttributeSearcher(first, dataSources, false, false, 0); - CommonAttributeSearchResults metadata = singleSourceBuilder.findFiles(); + CommonAttributeSearchResults metadata = singleSourceBuilder.findMatches(); Map objectIdToDataSource = mapFileInstancesToDataSources(metadata); @@ -266,7 +266,7 @@ public class IngestedWithHashAndFileTypeIntraCaseTests extends NbTestCase { Long first = getDataSourceIdByName(SET1, dataSources); AbstractCommonAttributeSearcher singleSourceBuilder = new SingleIntraCaseCommonAttributeSearcher(first, dataSources, true, false, 0); - CommonAttributeSearchResults metadata = singleSourceBuilder.findFiles(); + CommonAttributeSearchResults metadata = singleSourceBuilder.findMatches(); Map objectIdToDataSource = mapFileInstancesToDataSources(metadata); @@ -308,7 +308,7 @@ public class IngestedWithHashAndFileTypeIntraCaseTests extends NbTestCase { Long first = getDataSourceIdByName(SET1, dataSources); AbstractCommonAttributeSearcher singleSourceBuilder = new SingleIntraCaseCommonAttributeSearcher(first, dataSources, false, true, 0); - CommonAttributeSearchResults metadata = singleSourceBuilder.findFiles(); + CommonAttributeSearchResults metadata = singleSourceBuilder.findMatches(); Map objectIdToDataSource = mapFileInstancesToDataSources(metadata); @@ -350,7 +350,7 @@ public class IngestedWithHashAndFileTypeIntraCaseTests extends NbTestCase { Long second = getDataSourceIdByName(SET2, dataSources); AbstractCommonAttributeSearcher singleSourceBuilder = new SingleIntraCaseCommonAttributeSearcher(second, dataSources, false, false, 0); - CommonAttributeSearchResults metadata = singleSourceBuilder.findFiles(); + CommonAttributeSearchResults metadata = singleSourceBuilder.findMatches(); Map objectIdToDataSource = mapFileInstancesToDataSources(metadata); @@ -391,7 +391,7 @@ public class IngestedWithHashAndFileTypeIntraCaseTests extends NbTestCase { Long last = getDataSourceIdByName(SET4, dataSources); AbstractCommonAttributeSearcher singleSourceBuilder = new SingleIntraCaseCommonAttributeSearcher(last, dataSources, false, false, 0); - CommonAttributeSearchResults metadata = singleSourceBuilder.findFiles(); + CommonAttributeSearchResults metadata = singleSourceBuilder.findMatches(); Map objectIdToDataSource = mapFileInstancesToDataSources(metadata); @@ -432,7 +432,7 @@ public class IngestedWithHashAndFileTypeIntraCaseTests extends NbTestCase { Long third = getDataSourceIdByName(SET3, dataSources); AbstractCommonAttributeSearcher singleSourceBuilder = new SingleIntraCaseCommonAttributeSearcher(third, dataSources, false, false, 0); - CommonAttributeSearchResults metadata = singleSourceBuilder.findFiles(); + CommonAttributeSearchResults metadata = singleSourceBuilder.findMatches(); Map objectIdToDataSource = mapFileInstancesToDataSources(metadata); diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/IngestedWithNoFileTypesIntraCaseTests.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/IngestedWithNoFileTypesIntraCaseTests.java index dbca68d586..f10721838c 100644 --- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/IngestedWithNoFileTypesIntraCaseTests.java +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/IngestedWithNoFileTypesIntraCaseTests.java @@ -102,7 +102,7 @@ public class IngestedWithNoFileTypesIntraCaseTests extends NbTestCase { Map dataSources = this.utils.getDataSourceMap(); IntraCaseCommonAttributeSearcher allSourcesBuilder = new AllIntraCaseCommonAttributeSearcher(dataSources, true, false, 0); - CommonAttributeSearchResults metadata = allSourcesBuilder.findFiles(); + CommonAttributeSearchResults metadata = allSourcesBuilder.findMatches(); Map objectIdToDataSource = IntraCaseTestUtils.mapFileInstancesToDataSources(metadata); @@ -126,7 +126,7 @@ public class IngestedWithNoFileTypesIntraCaseTests extends NbTestCase { Long third = IntraCaseTestUtils.getDataSourceIdByName(IntraCaseTestUtils.SET3, dataSources); IntraCaseCommonAttributeSearcher singleSourceBuilder = new SingleIntraCaseCommonAttributeSearcher(third, dataSources, true, false, 0); - CommonAttributeSearchResults metadata = singleSourceBuilder.findFiles(); + CommonAttributeSearchResults metadata = singleSourceBuilder.findMatches(); Map objectIdToDataSource = IntraCaseTestUtils.mapFileInstancesToDataSources(metadata); diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/InterCaseTestUtils.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/InterCaseTestUtils.java index 2791f725a7..de9b537ff6 100644 --- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/InterCaseTestUtils.java +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/InterCaseTestUtils.java @@ -25,8 +25,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.apache.commons.io.FileUtils; import org.netbeans.junit.NbTestCase; @@ -59,6 +59,14 @@ import org.sleuthkit.autopsy.commonfilesearch.DataSourceLoader; import org.sleuthkit.autopsy.commonfilesearch.CommonAttributeValue; import org.sleuthkit.autopsy.commonfilesearch.CommonAttributeValueList; import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; +import org.sleuthkit.autopsy.modules.e01verify.E01VerifierModuleFactory; +import org.sleuthkit.autopsy.modules.embeddedfileextractor.EmbeddedFileExtractorModuleFactory; +import org.sleuthkit.autopsy.modules.exif.ExifParserModuleFactory; +import org.sleuthkit.autopsy.modules.fileextmismatch.FileExtMismatchDetectorModuleFactory; +import org.sleuthkit.autopsy.modules.iOS.iOSModuleFactory; +import org.sleuthkit.autopsy.modules.interestingitems.InterestingItemsIngestModuleFactory; +import org.sleuthkit.autopsy.modules.photoreccarver.PhotoRecCarverIngestModuleFactory; +import org.sleuthkit.autopsy.modules.vmextractor.VMExtractorIngestModuleFactory; import org.sleuthkit.datamodel.AbstractFile; /** @@ -73,23 +81,54 @@ import org.sleuthkit.datamodel.AbstractFile; * Description of Test Data: (Note: files of the same name and extension are * identical; files of the same name and differing extension are not identical.) * - * Case 1 +Data Set 1 - Hash-0.dat [testFile of size 0] - Hash-A.jpg - - * Hash-A.pdf +Data Set2 - Hash-0.dat [testFile of size 0] - Hash-A.jpg - - * Hash-A.pdf Case 2 +Data Set 1 - Hash-B.jpg - Hash-B.pdf +Data Set 2 - - * Hash-A.jpg - Hash-A.pdf - Hash_D.doc Case 3 +Data Set 1 - Hash-A.jpg - - * Hash-A.pdf - Hash-C.jpg - Hash-C.pdf - Hash-D.jpg +Data Set 2 - Hash-C.jpg - - * Hash-C.pdf - Hash-D.doc + * Case 1 + * +Data Set 1 + * - Hash-0.dat [testFile of size 0] + * - Hash-A.jpg + * - Hash-A.pdf + * + * +Data Set2 + * - Hash-0.dat [testFile of size 0] + * - Hash-A.jpg + * - Hash-A.pdf + * + * Case 2 + * +Data Set 1 + * - Hash-B.jpg + * - Hash-B.pdf + * +Data Set 2 + * - Hash-A.jpg + * - Hash-A.pdf + * - Hash_D.doc + * + * Case 3 + * +Data Set 1 + * - Hash-A.jpg + * - Hash-A.pdf + * - Hash-C.jpg + * - Hash-C.pdf + * - Hash-D.jpg + * +Data Set 2 + * - Hash-C.jpg + * - Hash-C.pdf + * - Hash-D.doc * * Frequency Breakdown (ratio of datasources a given file appears in to total * number of datasources): * - * Hash-0.dat - moot; these are always excluded Hash-A.jpg - 4/6 Hash-A.pdf - - * 4/6 Hash-B.jpg - 1/6 Hash-B.pdf - 1/6 Hash-C.jpg - 2/6 Hash-C.pdf - 2/6 - * Hash_D.doc - 2/6 Hash-D.jpg - 1/6 + * Hash-0.dat - moot; these are always excluded + * Hash-A.jpg - 4/6 + * Hash-A.pdf - 4/6 + * Hash-B.jpg - 1/6 + * Hash-B.pdf - 1/6 + * Hash-C.jpg - 2/6 + * Hash-C.pdf - 2/6 + * Hash_D.doc - 2/6 + * Hash-D.jpg - 1/6 * */ class InterCaseTestUtils { - + private static final Path CASE_DIRECTORY_PATH = Paths.get(System.getProperty("java.io.tmpdir"), "InterCaseCommonFilesSearchTest"); private static final String CR_DB_NAME = "testcentralrepo.db"; @@ -121,13 +160,31 @@ class InterCaseTestUtils { static final String CASE2_DATASET_2 = "c2ds2_v1.vhd"; static final String CASE3_DATASET_1 = "c3ds1_v1.vhd"; static final String CASE3_DATASET_2 = "c3ds2_v1.vhd"; - + + final Path attrCase1Path; + final Path attrCase2Path; + final Path attrCase3Path; + final Path attrCase4Path; + + static final String ATTR_CASE1 = "CommonFilesAttrs_img1_v1.vhd"; + static final String ATTR_CASE2 = "CommonFilesAttrs_img2_v1.vhd"; + static final String ATTR_CASE3 = "CommonFilesAttrs_img3_v1.vhd"; + static final String ATTR_CASE4 = "CommonFilesAttrs_img4_v1.vhd"; + private final ImageDSProcessor imageDSProcessor; private final IngestJobSettings hashAndFileType; private final IngestJobSettings hashAndNoFileType; + private final IngestJobSettings kitchenShink; + private final DataSourceLoader dataSourceLoader; - + + CorrelationAttributeInstance.Type FILE_TYPE; + CorrelationAttributeInstance.Type DOMAIN_TYPE; + CorrelationAttributeInstance.Type USB_ID_TYPE; + CorrelationAttributeInstance.Type EMAIL_TYPE; + CorrelationAttributeInstance.Type PHONE_TYPE; + InterCaseTestUtils(NbTestCase testCase) { this.case1DataSet1Path = Paths.get(testCase.getDataDir().toString(), CASE1_DATASET_1); @@ -136,13 +193,32 @@ class InterCaseTestUtils { this.case2DataSet2Path = Paths.get(testCase.getDataDir().toString(), CASE2_DATASET_2); this.case3DataSet1Path = Paths.get(testCase.getDataDir().toString(), CASE3_DATASET_1); this.case3DataSet2Path = Paths.get(testCase.getDataDir().toString(), CASE3_DATASET_2); + + this.attrCase1Path = Paths.get(testCase.getDataDir().toString(), ATTR_CASE1); + this.attrCase2Path = Paths.get(testCase.getDataDir().toString(), ATTR_CASE2); + this.attrCase3Path = Paths.get(testCase.getDataDir().toString(), ATTR_CASE3); + this.attrCase4Path = Paths.get(testCase.getDataDir().toString(), ATTR_CASE4); this.imageDSProcessor = new ImageDSProcessor(); - final IngestModuleTemplate hashLookupTemplate = IngestUtils.getIngestModuleTemplate(new HashLookupModuleFactory()); + final IngestModuleTemplate exifTemplate = IngestUtils.getIngestModuleTemplate(new ExifParserModuleFactory()); + final IngestModuleTemplate iOsTemplate = IngestUtils.getIngestModuleTemplate(new iOSModuleFactory()); + final IngestModuleTemplate embeddedFileExtractorTemplate = IngestUtils.getIngestModuleTemplate(new EmbeddedFileExtractorModuleFactory()); + final IngestModuleTemplate interestingItemsTemplate = IngestUtils.getIngestModuleTemplate(new InterestingItemsIngestModuleFactory()); final IngestModuleTemplate mimeTypeLookupTemplate = IngestUtils.getIngestModuleTemplate(new FileTypeIdModuleFactory()); + final IngestModuleTemplate hashLookupTemplate = IngestUtils.getIngestModuleTemplate(new HashLookupModuleFactory()); + final IngestModuleTemplate vmExtractorTemplate = IngestUtils.getIngestModuleTemplate(new VMExtractorIngestModuleFactory()); + final IngestModuleTemplate photoRecTemplate = IngestUtils.getIngestModuleTemplate(new PhotoRecCarverIngestModuleFactory()); + final IngestModuleTemplate e01VerifierTemplate = IngestUtils.getIngestModuleTemplate(new E01VerifierModuleFactory()); final IngestModuleTemplate eamDbTemplate = IngestUtils.getIngestModuleTemplate(new org.sleuthkit.autopsy.centralrepository.ingestmodule.IngestModuleFactory()); - + final IngestModuleTemplate fileExtMismatchDetectorTemplate = IngestUtils.getIngestModuleTemplate(new FileExtMismatchDetectorModuleFactory()); + //TODO we need to figure out how to get ahold of these objects because they are required for properly filling the CR with test data +// final IngestModuleTemplate objectDetectorTemplate = IngestUtils.getIngestModuleTemplate(new org.sleuthkit.autopsy.experimental.objectdetection.ObjectDetectionModuleFactory()); +// final IngestModuleTemplate emailParserTemplate = IngestUtils.getIngestModuleTemplate(new org.sleuthkit.autopsy.thunderbirdparser.EmailParserModuleFactory()); +// final IngestModuleTemplate recentActivityTemplate = IngestUtils.getIngestModuleTemplate(new org.sleuthkit.autopsy.recentactivity.RecentActivityExtracterModuleFactory()); +// final IngestModuleTemplate keywordSearchTemplate = IngestUtils.getIngestModuleTemplate(new org.sleuthkit.autopsy.keywordsearch.KeywordSearchModuleFactory()); + + //hash and mime ArrayList hashAndMimeTemplate = new ArrayList<>(2); hashAndMimeTemplate.add(hashLookupTemplate); hashAndMimeTemplate.add(mimeTypeLookupTemplate); @@ -150,13 +226,56 @@ class InterCaseTestUtils { this.hashAndFileType = new IngestJobSettings(InterCaseTestUtils.class.getCanonicalName(), IngestType.FILES_ONLY, hashAndMimeTemplate); + //hash and no mime ArrayList hashAndNoMimeTemplate = new ArrayList<>(1); hashAndNoMimeTemplate.add(hashLookupTemplate); hashAndMimeTemplate.add(eamDbTemplate); this.hashAndNoFileType = new IngestJobSettings(InterCaseTestUtils.class.getCanonicalName(), IngestType.FILES_ONLY, hashAndNoMimeTemplate); + //kitchen sink + ArrayList kitchenSink = new ArrayList<>(); + kitchenSink.add(exifTemplate); + kitchenSink.add(iOsTemplate); + kitchenSink.add(embeddedFileExtractorTemplate); + kitchenSink.add(interestingItemsTemplate); + kitchenSink.add(mimeTypeLookupTemplate); + kitchenSink.add(hashLookupTemplate); + kitchenSink.add(vmExtractorTemplate); + kitchenSink.add(photoRecTemplate); + kitchenSink.add(e01VerifierTemplate); + kitchenSink.add(eamDbTemplate); + kitchenSink.add(fileExtMismatchDetectorTemplate); + //TODO this list should probably be populated by way of loading the appropriate modules based on finding all of the @ServiceProvider(service = IngestModuleFactory.class) types +// kitchenSink.add(objectDetectorTemplate); +// kitchenSink.add(emailParserTemplate); +// kitchenSink.add(recentActivityTemplate); +// kitchenSink.add(keywordSearchTemplate); + + this.kitchenShink = new IngestJobSettings(InterCaseTestUtils.class.getCanonicalName(), IngestType.ALL_MODULES, kitchenSink); + this.dataSourceLoader = new DataSourceLoader(); + + try { + Collection types = CorrelationAttributeInstance.getDefaultCorrelationTypes(); + + //TODO use ids instead of strings + FILE_TYPE = types.stream().filter(type -> type.getDisplayName().equals("Files")).findAny().get(); + DOMAIN_TYPE = types.stream().filter(type -> type.getDisplayName().equals("Domains")).findAny().get(); + USB_ID_TYPE = types.stream().filter(type -> type.getDisplayName().equals("USB Devices")).findAny().get(); + EMAIL_TYPE = types.stream().filter(type -> type.getDisplayName().equals("Email Addresses")).findAny().get(); + PHONE_TYPE = types.stream().filter(type -> type.getDisplayName().equals("Phone Numbers")).findAny().get(); + + } catch (EamDbException ex) { + Assert.fail(ex.getMessage()); + + //none of this really matters but satisfies the compiler + FILE_TYPE = null; + DOMAIN_TYPE = null; + USB_ID_TYPE = null; + EMAIL_TYPE = null; + PHONE_TYPE = null; + } } void clearTestDir() { @@ -201,6 +320,10 @@ class InterCaseTestUtils { IngestJobSettings getIngestSettingsForHashAndNoFileType() { return this.hashAndNoFileType; } + + IngestJobSettings getIngestSettingsForKitchenSink(){ + return this.kitchenShink; + } void enableCentralRepo() throws EamDbException { @@ -214,7 +337,7 @@ class InterCaseTestUtils { crSettings.initializeDatabaseSchema(); crSettings.insertDefaultDatabaseContent(); - crSettings.saveSettings(); + crSettings.saveSettings(); EamDbUtil.setUseCentralRepo(true); EamDbPlatformEnum.setSelectedPlatform(EamDbPlatformEnum.SQLITE.name()); @@ -222,33 +345,33 @@ class InterCaseTestUtils { } /** - * Create 3 cases and ingest each with the given settings. Null settings are - * permitted but IngestUtils will not be run. + * Create the cases defined by caseNames and caseDataSourcePaths and ingest + * each with the given settings. Null settings are permitted but + * IngestUtils will not be run. + * + * 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 caseReferenceToStore */ - Case createCases(IngestJobSettings ingestJobSettings, String caseReferenceToStore) throws TskCoreException { + Case createCases(String[] caseNames, Path[][] caseDataSourcePaths, IngestJobSettings ingestJobSettings, String caseReferenceToStore) throws TskCoreException { Case currentCase = null; - String[] cases = new String[]{ - CASE1, - CASE2, - CASE3}; - - Path[][] paths = { - {this.case1DataSet1Path, this.case1DataSet2Path}, - {this.case2DataSet1Path, this.case2DataSet2Path}, - {this.case3DataSet1Path, this.case3DataSet2Path}}; + if(caseNames.length != caseDataSourcePaths.length){ + Assert.fail(new IllegalArgumentException("caseReferenceToStore should be one of the values given in the 'cases' parameter.").getMessage()); + } String lastCaseName = null; Path[] lastPathsForCase = null; //iterate over the collections above, creating cases, and storing // just one of them for future reference - for (int i = 0; i < cases.length; i++) { - String caseName = cases[i]; - Path[] pathsForCase = paths[i]; + for (int i = 0; i < caseNames.length; i++) { + String caseName = caseNames[i]; + Path[] pathsForCase = caseDataSourcePaths[i]; if (caseName.equals(caseReferenceToStore)) { //put aside and do this one last so we can hang onto the case @@ -266,7 +389,7 @@ class InterCaseTestUtils { } if (currentCase == null) { - Assert.fail(new IllegalArgumentException("caseReferenceToStore should be one of: CASE1, CASE2, CASE3").getMessage()); + Assert.fail(new IllegalArgumentException("caseReferenceToStore should be one of the values given in the 'cases' parameter.").getMessage()); return null; } else { return currentCase; @@ -289,6 +412,27 @@ class InterCaseTestUtils { return null; } } + + static boolean verifyInstanceCount(CommonAttributeSearchResults searchDomain, int instanceCount){ + try { + int tally = 0; + + for (Map.Entry entry : searchDomain.getMetadata().entrySet()) { + entry.getValue().displayDelayedMetadata(); + for (CommonAttributeValue value : entry.getValue().getMetadataList()) { + + tally += value.getInstanceCount(); + } + } + + return tally == instanceCount; + + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex.getMessage()); + return false; + } + } static boolean verifyInstanceExistanceAndCount(CommonAttributeSearchResults searchDomain, String fileName, String dataSource, String crCase, int instanceCount) { @@ -381,4 +525,29 @@ class InterCaseTestUtils { Assert.fail(ex.getMessage()); } } + + /** + * Is everything in metadata a result of the given attribute type? + * + * @param metadata + * @param attributeType + * @return true if yes, else false + */ + boolean areAllResultsOfType(CommonAttributeSearchResults metadata, CorrelationAttributeInstance.Type attributeType) { + try { + for(CommonAttributeValueList matches : metadata.getMetadata().values()){ + for(CommonAttributeValue value : matches.getMetadataList()){ + return value + .getInstances() + .stream() + .allMatch(inst -> inst.getCorrelationAttributeInstanceType().equals(attributeType)); + } + return false; + } + return false; + } catch (EamDbException ex) { + Assert.fail(ex.getMessage()); + return false; + } + } } diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/MatchesInAtLeastTwoSourcesIntraCaseTests.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/MatchesInAtLeastTwoSourcesIntraCaseTests.java index 1105628340..5021a335ca 100644 --- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/MatchesInAtLeastTwoSourcesIntraCaseTests.java +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/MatchesInAtLeastTwoSourcesIntraCaseTests.java @@ -106,7 +106,7 @@ public class MatchesInAtLeastTwoSourcesIntraCaseTests extends NbTestCase { Map dataSources = this.utils.getDataSourceMap(); AbstractCommonAttributeSearcher allSourcesBuilder = new AllIntraCaseCommonAttributeSearcher(dataSources, false, false, 0); - CommonAttributeSearchResults metadata = allSourcesBuilder.findFiles(); + CommonAttributeSearchResults metadata = allSourcesBuilder.findMatches(); Map objectIdToDataSource = IntraCaseTestUtils.mapFileInstancesToDataSources(metadata); diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/UningestedCasesIntraCaseTests.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/UningestedCasesIntraCaseTests.java index dd81ba63c9..7edce65dda 100644 --- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/UningestedCasesIntraCaseTests.java +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonfilessearch/UningestedCasesIntraCaseTests.java @@ -81,7 +81,7 @@ public class UningestedCasesIntraCaseTests extends NbTestCase { Map dataSources = this.utils.getDataSourceMap(); IntraCaseCommonAttributeSearcher allSourcesBuilder = new AllIntraCaseCommonAttributeSearcher(dataSources, false, false, 0); - CommonAttributeSearchResults metadata = allSourcesBuilder.findFiles(); + CommonAttributeSearchResults metadata = allSourcesBuilder.findMatches(); int resultCount = metadata.size(); assertEquals(resultCount, 0); @@ -101,7 +101,7 @@ public class UningestedCasesIntraCaseTests extends NbTestCase { Long first = getDataSourceIdByName(SET1, dataSources); IntraCaseCommonAttributeSearcher singleSourceBuilder = new SingleIntraCaseCommonAttributeSearcher(first, dataSources, false, false, 0); - CommonAttributeSearchResults metadata = singleSourceBuilder.findFiles(); + CommonAttributeSearchResults metadata = singleSourceBuilder.findMatches(); int resultCount = metadata.size(); assertEquals(resultCount, 0); diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/testutils/IngestJobRunner.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/testutils/IngestJobRunner.java index f3587ab29a..0abaebf48b 100755 --- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/testutils/IngestJobRunner.java +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/testutils/IngestJobRunner.java @@ -39,7 +39,7 @@ public final class IngestJobRunner { * Runs an ingest job, blocking until the job is completed. * * @param dataSources The data sources for the ingest job. - * @param settings The settings for the ingst job + * @param settings The settings for the ingest job * * @return A list of ingest module start up error messages, empty if the job * was started sucessfully. @@ -68,7 +68,7 @@ public final class IngestJobRunner { } /** - * IngestRunner instances cannot be instatiated. + * IngestRunner instances cannot be instantiated. */ private IngestJobRunner() { } diff --git a/CoreLibs/nbproject/project.xml b/CoreLibs/nbproject/project.xml index b2aa57c54e..38da548a38 100644 --- a/CoreLibs/nbproject/project.xml +++ b/CoreLibs/nbproject/project.xml @@ -230,6 +230,7 @@ org.apache.commons.codec.digest org.apache.commons.codec.language org.apache.commons.codec.net + org.apache.commons.collections4 org.apache.commons.csv org.apache.commons.io org.apache.commons.io.comparator diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java index cca4ec0f93..c983e6a721 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java @@ -21,21 +21,24 @@ package org.sleuthkit.autopsy.imagegallery; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.ThreadFactoryBuilder; -import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import javafx.application.Platform; import javafx.beans.Observable; +import javafx.beans.property.DoubleProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyDoubleProperty; -import javafx.beans.property.ReadOnlyDoubleWrapper; import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.ReadOnlyIntegerWrapper; +import javafx.beans.property.ReadOnlyLongWrapper; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.SimpleBooleanProperty; @@ -43,25 +46,14 @@ import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.concurrent.Worker; -import javafx.geometry.Insets; -import javafx.scene.Node; -import javafx.scene.control.ProgressIndicator; -import javafx.scene.layout.Background; -import javafx.scene.layout.BackgroundFill; -import javafx.scene.layout.CornerRadii; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import javafx.scene.paint.Color; -import javax.annotation.Nullable; -import javax.swing.SwingUtilities; +import javax.annotation.Nonnull; +import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import org.netbeans.api.progress.ProgressHandle; import org.openide.util.Cancellable; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.Case.CaseType; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; -import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; -import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.autopsy.coreutils.History; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; @@ -69,18 +61,18 @@ import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.imagegallery.actions.UndoRedoManager; import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB; +import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB.DrawableDbBuildStatusEnum; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager; import org.sleuthkit.autopsy.imagegallery.datamodel.HashSetManager; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; -import org.sleuthkit.autopsy.imagegallery.gui.NoGroupsDialog; -import org.sleuthkit.autopsy.imagegallery.gui.Toolbar; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.SleuthkitCase.CaseDbTransaction; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; @@ -90,8 +82,7 @@ import org.sleuthkit.datamodel.TskData; */ public final class ImageGalleryController { - private static final Logger LOGGER = Logger.getLogger(ImageGalleryController.class.getName()); - private static ImageGalleryController instance; + private static final Logger logger = Logger.getLogger(ImageGalleryController.class.getName()); /** * true if Image Gallery should listen to ingest events, false if it should @@ -103,7 +94,7 @@ public final class ImageGalleryController { private final ReadOnlyBooleanWrapper stale = new ReadOnlyBooleanWrapper(false); private final ReadOnlyBooleanWrapper metaDataCollapsed = new ReadOnlyBooleanWrapper(false); - private final ReadOnlyDoubleWrapper thumbnailSize = new ReadOnlyDoubleWrapper(100); + private final SimpleDoubleProperty thumbnailSizeProp = new SimpleDoubleProperty(100); private final ReadOnlyBooleanWrapper regroupDisabled = new ReadOnlyBooleanWrapper(false); private final ReadOnlyIntegerWrapper dbTaskQueueSize = new ReadOnlyIntegerWrapper(0); @@ -111,36 +102,23 @@ public final class ImageGalleryController { private final History historyManager = new History<>(); private final UndoRedoManager undoManager = new UndoRedoManager(); - private final GroupManager groupManager = new GroupManager(this); - private final HashSetManager hashSetManager = new HashSetManager(); - private final CategoryManager categoryManager = new CategoryManager(this); - private final DrawableTagsManager tagsManager = new DrawableTagsManager(null); - - private Runnable showTree; - private Toolbar toolbar; - private StackPane fullUIStackPane; - private StackPane centralStackPane; - private Node infoOverlay; - private final Region infoOverLayBackground = new Region() { - { - setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY))); - setOpacity(.4); - } - }; + private final ThumbnailCache thumbnailCache = new ThumbnailCache(this); + private final GroupManager groupManager; + private final HashSetManager hashSetManager; + private final CategoryManager categoryManager; + private final DrawableTagsManager tagsManager; private ListeningExecutorService dbExecutor; - private SleuthkitCase sleuthKitCase; - private DrawableDB db; + private final Case autopsyCase; + private final SleuthkitCase sleuthKitCase; + private final DrawableDB drawableDB; - public static synchronized ImageGalleryController getDefault() { - if (instance == null) { - instance = new ImageGalleryController(); - } - return instance; + public Case getAutopsyCase() { + return autopsyCase; } - public ReadOnlyBooleanProperty getMetaDataCollapsed() { + public ReadOnlyBooleanProperty metaDataCollapsedProperty() { return metaDataCollapsed.getReadOnlyProperty(); } @@ -148,19 +126,19 @@ public final class ImageGalleryController { this.metaDataCollapsed.set(metaDataCollapsed); } - public ReadOnlyDoubleProperty thumbnailSizeProperty() { - return thumbnailSize.getReadOnlyProperty(); + public DoubleProperty thumbnailSizeProperty() { + return thumbnailSizeProp; } - private GroupViewState getViewState() { + public GroupViewState getViewState() { return historyManager.getCurrentState(); } - public ReadOnlyBooleanProperty regroupDisabled() { + public ReadOnlyBooleanProperty regroupDisabledProperty() { return regroupDisabled.getReadOnlyProperty(); } - public ReadOnlyObjectProperty viewState() { + public ReadOnlyObjectProperty viewStateProperty() { return historyManager.currentState(); } @@ -172,8 +150,8 @@ public final class ImageGalleryController { return groupManager; } - synchronized public DrawableDB getDatabase() { - return db; + public DrawableDB getDatabase() { + return drawableDB; } public void setListeningEnabled(boolean enabled) { @@ -193,14 +171,9 @@ public final class ImageGalleryController { Platform.runLater(() -> { stale.set(b); }); - try { - new PerCaseProperties(Case.getCurrentCaseThrows()).setConfigSetting(ImageGalleryModule.getModuleName(), PerCaseProperties.STALE, b.toString()); - } catch (NoCurrentCaseException ex) { - Logger.getLogger(ImageGalleryController.class.getName()).log(Level.WARNING, "Exception while getting open case."); //NON-NLS - } } - public ReadOnlyBooleanProperty stale() { + public ReadOnlyBooleanProperty staleProperty() { return stale.getReadOnlyProperty(); } @@ -209,50 +182,57 @@ public final class ImageGalleryController { return stale.get(); } - private ImageGalleryController() { + ImageGalleryController(@Nonnull Case newCase) throws TskCoreException { - listeningEnabled.addListener((observable, oldValue, newValue) -> { + this.autopsyCase = Objects.requireNonNull(newCase); + this.sleuthKitCase = newCase.getSleuthkitCase(); + + setListeningEnabled(ImageGalleryModule.isEnabledforCase(newCase)); + + groupManager = new GroupManager(this); + this.drawableDB = DrawableDB.getDrawableDB(this); + categoryManager = new CategoryManager(this); + tagsManager = new DrawableTagsManager(this); + tagsManager.registerListener(groupManager); + tagsManager.registerListener(categoryManager); + + hashSetManager = new HashSetManager(drawableDB); + setStale(isDataSourcesTableStale()); + + dbExecutor = getNewDBExecutor(); + + // listener for the boolean property about when IG is listening / enabled + listeningEnabled.addListener((observable, wasPreviouslyEnabled, isEnabled) -> { try { - //if we just turned on listening and a case is open and that case is not up to date - if (newValue && !oldValue && ImageGalleryModule.isDrawableDBStale(Case.getCurrentCaseThrows())) { + // if we just turned on listening and a single-user case is open and that case is not up to date, then rebuild it + // For multiuser cases, we defer DB rebuild till the user actually opens Image Gallery + if (isEnabled && !wasPreviouslyEnabled + && isDataSourcesTableStale() + && (Case.getCurrentCaseThrows().getCaseType() == CaseType.SINGLE_USER_CASE)) { //populate the db - queueDBTask(new CopyAnalyzedFiles(instance, db, sleuthKitCase)); + this.rebuildDB(); } + } catch (NoCurrentCaseException ex) { - LOGGER.log(Level.WARNING, "Exception while getting open case.", ex); + logger.log(Level.WARNING, "Exception while getting open case.", ex); } }); - groupManager.getAnalyzedGroups().addListener((Observable o) -> { - //analyzed groups is confined to JFX thread - if (Case.isCaseOpen()) { - checkForGroups(); - } - }); - - groupManager.getUnSeenGroups().addListener((Observable observable) -> { - //if there are unseen groups and none being viewed - if (groupManager.getUnSeenGroups().isEmpty() == false && (getViewState() == null || getViewState().getGroup() == null)) { - advance(GroupViewState.tile(groupManager.getUnSeenGroups().get(0)), true); - } - }); - - viewState().addListener((Observable observable) -> { + viewStateProperty().addListener((Observable observable) -> { //when the viewed group changes, clear the selection and the undo/redo history selectionModel.clearSelection(); undoManager.clear(); }); - regroupDisabled.addListener(observable -> checkForGroups()); - IngestManager ingestManager = IngestManager.getInstance(); - PropertyChangeListener ingestEventHandler = - propertyChangeEvent -> Platform.runLater(this::updateRegroupDisabled); + PropertyChangeListener ingestEventHandler + = propertyChangeEvent -> Platform.runLater(this::updateRegroupDisabled); ingestManager.addIngestModuleEventListener(ingestEventHandler); ingestManager.addIngestJobEventListener(ingestEventHandler); dbTaskQueueSize.addListener(obs -> this.updateRegroupDisabled()); + } public ReadOnlyBooleanProperty getCanAdvance() { @@ -264,10 +244,7 @@ public final class ImageGalleryController { } @ThreadConfined(type = ThreadConfined.ThreadType.ANY) - public void advance(GroupViewState newState, boolean forceShowTree) { - if (forceShowTree && showTree != null) { - showTree.run(); - } + public void advance(GroupViewState newState) { historyManager.advance(newState); } @@ -284,131 +261,92 @@ public final class ImageGalleryController { regroupDisabled.set((dbTaskQueueSize.get() > 0) || IngestManager.getInstance().isIngestRunning()); } - /** - * Check if there are any fully analyzed groups available from the - * GroupManager and remove blocking progress spinners if there are. If there - * aren't, add a blocking progress spinner with appropriate message. - */ - @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - @NbBundle.Messages({"ImageGalleryController.noGroupsDlg.msg1=No groups are fully analyzed; but listening to ingest is disabled. " - + " No groups will be available until ingest is finished and listening is re-enabled.", - "ImageGalleryController.noGroupsDlg.msg2=No groups are fully analyzed yet, but ingest is still ongoing. Please Wait.", - "ImageGalleryController.noGroupsDlg.msg3=No groups are fully analyzed yet, but image / video data is still being populated. Please Wait.", - "ImageGalleryController.noGroupsDlg.msg4=There are no images/videos available from the added datasources; but listening to ingest is disabled. " - + " No groups will be available until ingest is finished and listening is re-enabled.", - "ImageGalleryController.noGroupsDlg.msg5=There are no images/videos in the added datasources.", - "ImageGalleryController.noGroupsDlg.msg6=There are no fully analyzed groups to display:" - + " the current Group By setting resulted in no groups, " - + "or no groups are fully analyzed but ingest is not running."}) - synchronized private void checkForGroups() { - if (groupManager.getAnalyzedGroups().isEmpty()) { - if (IngestManager.getInstance().isIngestRunning()) { - if (listeningEnabled.not().get()) { - replaceNotification(fullUIStackPane, - new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg1())); - } else { - replaceNotification(fullUIStackPane, - new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg2(), - new ProgressIndicator())); - } - - } else if (dbTaskQueueSize.get() > 0) { - replaceNotification(fullUIStackPane, - new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg3(), - new ProgressIndicator())); - } else if (db != null && db.countAllFiles() <= 0) { // there are no files in db - if (listeningEnabled.not().get()) { - replaceNotification(fullUIStackPane, - new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg4())); - } else { - replaceNotification(fullUIStackPane, - new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg5())); - } - - } else if (!groupManager.isRegrouping()) { - replaceNotification(centralStackPane, - new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg6())); - } - - } else { - clearNotification(); - } - } - - @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - private void clearNotification() { - //remove the ingest spinner - if (fullUIStackPane != null) { - fullUIStackPane.getChildren().remove(infoOverlay); - } - //remove the ingest spinner - if (centralStackPane != null) { - centralStackPane.getChildren().remove(infoOverlay); - } - } - - @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - private void replaceNotification(StackPane stackPane, Node newNode) { - clearNotification(); - - infoOverlay = new StackPane(infoOverLayBackground, newNode); - if (stackPane != null) { - stackPane.getChildren().add(infoOverlay); - } - } - /** * configure the controller for a specific case. * * @param theNewCase the case to configure the controller for + * + * @throws org.sleuthkit.datamodel.TskCoreException */ - public synchronized void setCase(Case theNewCase) { - if (null == theNewCase) { - reset(); - } else { - this.sleuthKitCase = theNewCase.getSleuthkitCase(); - this.db = DrawableDB.getDrawableDB(ImageGalleryModule.getModuleOutputDir(theNewCase), this); - - setListeningEnabled(ImageGalleryModule.isEnabledforCase(theNewCase)); - setStale(ImageGalleryModule.isDrawableDBStale(theNewCase)); - - // if we add this line icons are made as files are analyzed rather than on demand. - // db.addUpdatedFileListener(IconCache.getDefault()); - historyManager.clear(); - groupManager.setDB(db); - hashSetManager.setDb(db); - categoryManager.setDb(db); - tagsManager.setAutopsyTagsManager(theNewCase.getServices().getTagsManager()); - tagsManager.registerListener(groupManager); - tagsManager.registerListener(categoryManager); - shutDownDBExecutor(); - dbExecutor = getNewDBExecutor(); - } + /** + * Rebuilds the DrawableDB database. + * + */ + public void rebuildDB() { + // queue a rebuild task for each stale data source + getStaleDataSourceIds().forEach(dataSourceObjId -> queueDBTask(new CopyAnalyzedFiles(dataSourceObjId, this))); } /** * reset the state of the controller (eg if the case is closed) */ public synchronized void reset() { - LOGGER.info("resetting ImageGalleryControler to initial state."); //NON-NLS + logger.info("Closing ImageGalleryControler for case."); //NON-NLS + selectionModel.clearSelection(); - setListeningEnabled(false); - ThumbnailCache.getDefault().clearCache(); + thumbnailCache.clearCache(); historyManager.clear(); - groupManager.clear(); - tagsManager.clearFollowUpTagName(); - tagsManager.unregisterListener(groupManager); - tagsManager.unregisterListener(categoryManager); + groupManager.reset(); + shutDownDBExecutor(); + dbExecutor = getNewDBExecutor(); + } - if (toolbar != null) { - toolbar.reset(); + /** + * Checks if the datasources table in drawable DB is stale. + * + * @return true if datasources table is stale + */ + public boolean isDataSourcesTableStale() { + return isNotEmpty(getStaleDataSourceIds()); + } + + /** + * Returns a set of data source object ids that are stale. + * + * This includes any data sources already in the table, that are not in + * COMPLETE status, or any data sources that might have been added to the + * case, but are not in the datasources table. + * + * @return list of data source object ids that are stale. + */ + Set getStaleDataSourceIds() { + + Set staleDataSourceIds = new HashSet<>(); + + // no current case open to check + if ((null == getDatabase()) || (null == getSleuthKitCase())) { + return staleDataSourceIds; } - if (db != null) { - db.closeDBCon(); + try { + Map knownDataSourceIds = getDatabase().getDataSourceDbBuildStatus(); + + List dataSources = getSleuthKitCase().getDataSources(); + Set caseDataSourceIds = new HashSet<>(); + dataSources.stream().map(DataSource::getId).forEach(caseDataSourceIds::add); + + // collect all data sources already in the table, that are not yet COMPLETE + knownDataSourceIds.entrySet().stream().forEach((Map.Entry t) -> { + DrawableDbBuildStatusEnum status = t.getValue(); + if (DrawableDbBuildStatusEnum.COMPLETE != status) { + staleDataSourceIds.add(t.getKey()); + } + }); + + // collect any new data sources in the case. + caseDataSourceIds.forEach((Long id) -> { + if (!knownDataSourceIds.containsKey(id)) { + staleDataSourceIds.add(id); + } + }); + + return staleDataSourceIds; + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Image Gallery failed to check if datasources table is stale.", ex); + return staleDataSourceIds; } - db = null; + } synchronized private void shutDownDBExecutor() { @@ -417,7 +355,7 @@ public final class ImageGalleryController { try { dbExecutor.awaitTermination(30, TimeUnit.SECONDS); } catch (InterruptedException ex) { - LOGGER.log(Level.WARNING, "Image Gallery failed to shutdown DB Task Executor in a timely fashion.", ex); + logger.log(Level.WARNING, "Image Gallery failed to shutdown DB Task Executor in a timely fashion.", ex); } } } @@ -449,46 +387,14 @@ public final class ImageGalleryController { Platform.runLater(() -> dbTaskQueueSize.set(dbTaskQueueSize.get() - 1)); } - @Nullable - synchronized public DrawableFile getFileFromId(Long fileID) throws TskCoreException { - if (Objects.isNull(db)) { - LOGGER.log(Level.WARNING, "Could not get file from id, no DB set. The case is probably closed."); //NON-NLS - return null; - } - return db.getFileFromID(fileID); - } - - public void setStacks(StackPane fullUIStack, StackPane centralStack) { - fullUIStackPane = fullUIStack; - this.centralStackPane = centralStack; - Platform.runLater(this::checkForGroups); - } - - public synchronized void setToolbar(Toolbar toolbar) { - if (this.toolbar != null) { - throw new IllegalStateException("Can not set the toolbar a second time!"); - } - this.toolbar = toolbar; - thumbnailSize.bind(toolbar.thumbnailSizeProperty()); + public DrawableFile getFileFromID(Long fileID) throws TskCoreException { + return drawableDB.getFileFromID(fileID); } public ReadOnlyDoubleProperty regroupProgress() { return groupManager.regroupProgress(); } - /** - * invoked by {@link OnStart} to make sure that the ImageGallery listeners - * get setup as early as possible, and do other setup stuff. - */ - void onStart() { - Platform.setImplicitExit(false); - LOGGER.info("setting up ImageGallery listeners"); //NON-NLS - //TODO can we do anything usefull in an InjestJobEventListener? - //IngestManager.getInstance().addIngestJobEventListener((PropertyChangeEvent evt) -> {}); - IngestManager.getInstance().addIngestModuleEventListener(new IngestModuleEventListener()); - Case.addPropertyChangeListener(new CaseEventListener()); - } - public HashSetManager getHashSetManager() { return hashSetManager; } @@ -501,10 +407,6 @@ public final class ImageGalleryController { return tagsManager; } - public void setShowTree(Runnable showTree) { - this.showTree = showTree; - } - public UndoRedoManager getUndoManager() { return undoManager; } @@ -515,6 +417,12 @@ public final class ImageGalleryController { public synchronized SleuthkitCase getSleuthKitCase() { return sleuthKitCase; + + } + + public ThumbnailCache getThumbsCache() { + return thumbnailCache; + } /** @@ -594,7 +502,7 @@ public final class ImageGalleryController { return file; } - public FileTask(AbstractFile f, DrawableDB taskDB) { + FileTask(AbstractFile f, DrawableDB taskDB) { super(); this.file = f; this.taskDB = taskDB; @@ -604,7 +512,7 @@ public final class ImageGalleryController { /** * task that updates one file in database with results from ingest */ - static private class UpdateFileTask extends FileTask { + static class UpdateFileTask extends FileTask { UpdateFileTask(AbstractFile f, DrawableDB taskDB) { super(f, taskDB); @@ -631,7 +539,7 @@ public final class ImageGalleryController { /** * task that updates one file in database with results from ingest */ - static private class RemoveFileTask extends FileTask { + static class RemoveFileTask extends FileTask { RemoveFileTask(AbstractFile f, DrawableDB taskDB) { super(f, taskDB); @@ -654,51 +562,74 @@ public final class ImageGalleryController { } } + /** + * Base abstract class for various methods of copying image files data, for + * a given data source, into the Image gallery DB. + */ @NbBundle.Messages({"BulkTask.committingDb.status=committing image/video database", "BulkTask.stopCopy.status=Stopping copy to drawable db task.", "BulkTask.errPopulating.errMsg=There was an error populating Image Gallery database."}) - /* Base abstract class for various methods of copying data into the Image gallery DB */ - abstract static private class BulkTransferTask extends BackgroundTask { + abstract static class BulkTransferTask extends BackgroundTask { - static private final String FILE_EXTENSION_CLAUSE = - "(name LIKE '%." //NON-NLS - + String.join("' OR name LIKE '%.", FileTypeUtils.getAllSupportedExtensions()) //NON-NLS - + "')"; + static private final String FILE_EXTENSION_CLAUSE + = "(extension LIKE '" //NON-NLS + + String.join("' OR extension LIKE '", FileTypeUtils.getAllSupportedExtensions()) //NON-NLS + + "') "; - static private final String MIMETYPE_CLAUSE = - "(mime_type LIKE '" //NON-NLS - + String.join("' OR mime_type LIKE '", FileTypeUtils.getAllSupportedMimeTypes()) //NON-NLS - + "') "; + static private final String MIMETYPE_CLAUSE + = "(mime_type LIKE '" //NON-NLS + + String.join("' OR mime_type LIKE '", FileTypeUtils.getAllSupportedMimeTypes()) //NON-NLS + + "') "; - static final String DRAWABLE_QUERY = - //grab files with supported extension - "(" + FILE_EXTENSION_CLAUSE - //grab files with supported mime-types - + " OR " + MIMETYPE_CLAUSE //NON-NLS - //grab files with image or video mime-types even if we don't officially support them - + " OR mime_type LIKE 'video/%' OR mime_type LIKE 'image/%' )"; //NON-NLS + private final String DRAWABLE_QUERY; + private final String DATASOURCE_CLAUSE; - final ImageGalleryController controller; - final DrawableDB taskDB; - final SleuthkitCase tskCase; + protected final ImageGalleryController controller; + protected final DrawableDB taskDB; + protected final SleuthkitCase tskCase; + protected final long dataSourceObjId; - ProgressHandle progressHandle; + private ProgressHandle progressHandle; + private boolean taskCompletionStatus; - BulkTransferTask(ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) { + BulkTransferTask(long dataSourceObjId, ImageGalleryController controller) { this.controller = controller; - this.taskDB = taskDB; - this.tskCase = tskCase; + this.taskDB = controller.getDatabase(); + this.tskCase = controller.getSleuthKitCase(); + this.dataSourceObjId = dataSourceObjId; + + DATASOURCE_CLAUSE = " (data_source_obj_id = " + dataSourceObjId + ") "; + + DRAWABLE_QUERY + = DATASOURCE_CLAUSE + + " AND ( " + + //grab files with supported extension + FILE_EXTENSION_CLAUSE + //grab files with supported mime-types + + " OR " + MIMETYPE_CLAUSE //NON-NLS + //grab files with image or video mime-types even if we don't officially support them + + " OR mime_type LIKE 'video/%' OR mime_type LIKE 'image/%' )"; //NON-NLS } /** - * + * Do any cleanup for this task. + * * @param success true if the transfer was successful */ abstract void cleanup(boolean success); - abstract List getFiles() throws TskCoreException; + abstract void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr, CaseDbTransaction caseDBTransaction) throws TskCoreException; - abstract void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr) throws TskCoreException; + /** + * Gets a list of files to process. + * + * @return list of files to process + * + * @throws TskCoreException + */ + List getFiles() throws TskCoreException { + return tskCase.findAllFilesWhere(DRAWABLE_QUERY); + } @Override public void run() { @@ -706,24 +637,32 @@ public final class ImageGalleryController { progressHandle.start(); updateMessage(Bundle.CopyAnalyzedFiles_populatingDb_status()); + DrawableDB.DrawableTransaction drawableDbTransaction = null; + CaseDbTransaction caseDbTransaction = null; try { //grab all files with supported extension or detected mime types final List files = getFiles(); progressHandle.switchToDeterminate(files.size()); + taskDB.insertOrUpdateDataSource(dataSourceObjId, DrawableDB.DrawableDbBuildStatusEnum.IN_PROGRESS); + updateProgress(0.0); + taskCompletionStatus = true; + int workDone = 0; //do in transaction - DrawableDB.DrawableTransaction tr = taskDB.beginTransaction(); - int workDone = 0; + drawableDbTransaction = taskDB.beginTransaction(); + caseDbTransaction = tskCase.beginTransaction(); for (final AbstractFile f : files) { if (isCancelled() || Thread.interrupted()) { - LOGGER.log(Level.WARNING, "Task cancelled: not all contents may be transfered to drawable database."); //NON-NLS + logger.log(Level.WARNING, "Task cancelled or interrupted: not all contents may be transfered to drawable database."); //NON-NLS + taskCompletionStatus = false; progressHandle.finish(); + break; } - processFile(f, tr); + processFile(f, drawableDbTransaction, caseDbTransaction); workDone++; progressHandle.progress(f.getName(), workDone); @@ -737,23 +676,42 @@ public final class ImageGalleryController { updateProgress(1.0); progressHandle.start(); - taskDB.commitTransaction(tr, true); + caseDbTransaction.commit(); + taskDB.commitTransaction(drawableDbTransaction, true); } catch (TskCoreException ex) { + if (null != drawableDbTransaction) { + taskDB.rollbackTransaction(drawableDbTransaction); + } + if (null != caseDbTransaction) { + try { + caseDbTransaction.rollback(); + } catch (TskCoreException ex2) { + logger.log(Level.SEVERE, "Error in trying to rollback transaction", ex2); //NON-NLS + } + } + progressHandle.progress(Bundle.BulkTask_stopCopy_status()); - LOGGER.log(Level.WARNING, "Stopping copy to drawable db task. Failed to transfer all database contents", ex); //NON-NLS + logger.log(Level.WARNING, "Stopping copy to drawable db task. Failed to transfer all database contents", ex); //NON-NLS MessageNotifyUtil.Notify.warn(Bundle.BulkTask_errPopulating_errMsg(), ex.getMessage()); cleanup(false); return; } finally { progressHandle.finish(); + if (taskCompletionStatus) { + taskDB.insertOrUpdateDataSource(dataSourceObjId, DrawableDB.DrawableDbBuildStatusEnum.COMPLETE); + } updateMessage(""); updateProgress(-1.0); } - cleanup(true); + cleanup(taskCompletionStatus); } abstract ProgressHandle getInitialProgressHandle(); + + protected void setTaskCompletionStatus(boolean status) { + taskCompletionStatus = status; + } } /** @@ -766,24 +724,21 @@ public final class ImageGalleryController { @NbBundle.Messages({"CopyAnalyzedFiles.committingDb.status=committing image/video database", "CopyAnalyzedFiles.stopCopy.status=Stopping copy to drawable db task.", "CopyAnalyzedFiles.errPopulating.errMsg=There was an error populating Image Gallery database."}) - static private class CopyAnalyzedFiles extends BulkTransferTask { + static class CopyAnalyzedFiles extends BulkTransferTask { - CopyAnalyzedFiles(ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) { - super(controller, taskDB, tskCase); + CopyAnalyzedFiles(long dataSourceObjId, ImageGalleryController controller) { + super(dataSourceObjId, controller); } @Override protected void cleanup(boolean success) { - controller.setStale(!success); + // at the end of the task, set the stale status based on the + // cumulative status of all data sources + controller.setStale(controller.isDataSourcesTableStale()); } @Override - List getFiles() throws TskCoreException { - return tskCase.findAllFilesWhere(DRAWABLE_QUERY); - } - - @Override - void processFile(AbstractFile f, DrawableDB.DrawableTransaction tr) { + void processFile(AbstractFile f, DrawableDB.DrawableTransaction tr, CaseDbTransaction caseDbTransaction) throws TskCoreException { final boolean known = f.getKnown() == TskData.FileKnown.KNOWN; if (known) { @@ -791,13 +746,21 @@ public final class ImageGalleryController { } else { try { - if (FileTypeUtils.hasDrawableMIMEType(f)) { //supported mimetype => analyzed - taskDB.updateFile(DrawableFile.create(f, true, false), tr); - } else { //unsupported mimtype => analyzed but shouldn't include - taskDB.removeFile(f.getId(), tr); + //supported mimetype => analyzed + if (null != f.getMIMEType() && FileTypeUtils.hasDrawableMIMEType(f)) { + taskDB.updateFile(DrawableFile.create(f, true, false), tr, caseDbTransaction); + } else { + // if mimetype of the file hasn't been ascertained, ingest might not have completed yet. + if (null == f.getMIMEType()) { + // set to false to force the DB to be marked as stale + this.setTaskCompletionStatus(false); + } else { + //unsupported mimtype => analyzed but shouldn't include + taskDB.removeFile(f.getId(), tr); + } } } catch (FileTypeDetector.FileTypeDetectorInitException ex) { - throw new RuntimeException(ex); + throw new TskCoreException("Failed to initialize FileTypeDetector.", ex); } } } @@ -813,24 +776,17 @@ public final class ImageGalleryController { * Copy files from a newly added data source into the DB. Get all "drawable" * files, based on extension and mime-type. After ingest we use file type id * module and if necessary jpeg/png signature matching to add/remove files - * - * TODO: create methods to simplify progress value/text updates to both - * netbeans and ImageGallery progress/status */ @NbBundle.Messages({"PrePopulateDataSourceFiles.committingDb.status=committing image/video database"}) - static private class PrePopulateDataSourceFiles extends BulkTransferTask { - - private static final Logger LOGGER = Logger.getLogger(PrePopulateDataSourceFiles.class.getName()); - - private final Content dataSource; + static class PrePopulateDataSourceFiles extends BulkTransferTask { /** - * - * @param dataSourceId Data source object ID + * @param dataSourceObjId The object ID of the DataSource that is being + * pre-populated into the DrawableDB. + * @param controller The controller for this task. */ - PrePopulateDataSourceFiles(Content dataSource, ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) { - super(controller, taskDB, tskCase); - this.dataSource = dataSource; + PrePopulateDataSourceFiles(long dataSourceObjId, ImageGalleryController controller) { + super(dataSourceObjId, controller); } @Override @@ -838,14 +794,8 @@ public final class ImageGalleryController { } @Override - void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr) { - taskDB.insertFile(DrawableFile.create(f, false, false), tr); - } - - @Override - List getFiles() throws TskCoreException { - long datasourceID = dataSource.getDataSource().getId(); - return tskCase.findAllFilesWhere("data_source_obj_id = " + datasourceID + " AND " + DRAWABLE_QUERY); + void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr, CaseDbTransaction caseDBTransaction) { + taskDB.insertFile(DrawableFile.create(f, false, false), tr, caseDBTransaction); } @Override @@ -854,116 +804,4 @@ public final class ImageGalleryController { return ProgressHandle.createHandle(Bundle.PrePopulateDataSourceFiles_prepopulatingDb_status(), this); } } - - private class IngestModuleEventListener implements PropertyChangeListener { - - @Override - public void propertyChange(PropertyChangeEvent evt) { - if (RuntimeProperties.runningWithGUI() == false) { - /* - * Running in "headless" mode, no need to process any events. - * This cannot be done earlier because the switch to core - * components inactive may not have been made at start up. - */ - IngestManager.getInstance().removeIngestModuleEventListener(this); - return; - } - switch (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName())) { - case CONTENT_CHANGED: - //TODO: do we need to do anything here? -jm - case DATA_ADDED: - /* - * we could listen to DATA events and progressivly update - * files, and get data from DataSource ingest modules, but - * given that most modules don't post new artifacts in the - * events and we would have to query for them, without - * knowing which are the new ones, we just ignore these - * events for now. The relevant data should all be captured - * by file done event, anyways -jm - */ - break; - case FILE_DONE: - /** - * getOldValue has fileID getNewValue has - * {@link Abstractfile} - */ - - AbstractFile file = (AbstractFile) evt.getNewValue(); - - if (isListeningEnabled()) { - if (file.isFile()) { - try { - synchronized (ImageGalleryController.this) { - if (ImageGalleryModule.isDrawableAndNotKnown(file)) { - //this file should be included and we don't already know about it from hash sets (NSRL) - queueDBTask(new UpdateFileTask(file, db)); - } else if (FileTypeUtils.getAllSupportedExtensions().contains(file.getNameExtension())) { - //doing this check results in fewer tasks queued up, and faster completion of db update - //this file would have gotten scooped up in initial grab, but actually we don't need it - queueDBTask(new RemoveFileTask(file, db)); - } - } - } catch (TskCoreException | FileTypeDetector.FileTypeDetectorInitException ex) { - //TODO: What to do here? - LOGGER.log(Level.SEVERE, "Unable to determine if file is drawable and not known. Not making any changes to DB", ex); //NON-NLS - MessageNotifyUtil.Notify.error("Image Gallery Error", - "Unable to determine if file is drawable and not known. Not making any changes to DB. See the logs for details."); - } - } - } else { //TODO: keep track of what we missed for later - setStale(true); - } - break; - } - } - } - - private class CaseEventListener implements PropertyChangeListener { - - @Override - public void propertyChange(PropertyChangeEvent evt) { - if (RuntimeProperties.runningWithGUI() == false) { - /* - * Running in "headless" mode, no need to process any events. - * This cannot be done earlier because the switch to core - * components inactive may not have been made at start up. - */ - Case.removePropertyChangeListener(this); - return; - } - switch (Case.Events.valueOf(evt.getPropertyName())) { - case CURRENT_CASE: - Case newCase = (Case) evt.getNewValue(); - if (newCase == null) { // case is closing - //close window, reset everything - SwingUtilities.invokeLater(ImageGalleryTopComponent::closeTopComponent); - reset(); - } else { // a new case has been opened - setCase(newCase); //connect db, groupmanager, start worker thread - } - break; - case DATA_SOURCE_ADDED: - //copy all file data to drawable databse - Content newDataSource = (Content) evt.getNewValue(); - if (isListeningEnabled()) { - queueDBTask(new PrePopulateDataSourceFiles(newDataSource, ImageGalleryController.this, getDatabase(), getSleuthKitCase())); - } else {//TODO: keep track of what we missed for later - setStale(true); - } - break; - case CONTENT_TAG_ADDED: - final ContentTagAddedEvent tagAddedEvent = (ContentTagAddedEvent) evt; - if (getDatabase().isInDB(tagAddedEvent.getAddedTag().getContent().getId())) { - getTagsManager().fireTagAddedEvent(tagAddedEvent); - } - break; - case CONTENT_TAG_DELETED: - final ContentTagDeletedEvent tagDeletedEvent = (ContentTagDeletedEvent) evt; - if (getDatabase().isInDB(tagDeletedEvent.getDeletedTagInfo().getContentID())) { - getTagsManager().fireTagDeletedEvent(tagDeletedEvent); - } - break; - } - } - } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java index a5f13c1d9a..07baaf72a9 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-15 Basis Technology Corp. + * Copyright 2013-18 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,32 +18,77 @@ */ package org.sleuthkit.autopsy.imagegallery; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; import java.nio.file.Path; import java.nio.file.Paths; -import org.apache.commons.lang3.StringUtils; +import java.util.logging.Level; +import javafx.application.Platform; +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; import static org.apache.commons.lang3.StringUtils.isNotBlank; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; +import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.IngestManager.IngestJobEvent; +import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.FILE_DONE; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; -/** static definitions and utilities for the ImageGallery module */ +/** static definitions, utilities, and listeners for the ImageGallery module */ @NbBundle.Messages({"ImageGalleryModule.moduleName=Image Gallery"}) public class ImageGalleryModule { - private static final Logger LOGGER = Logger.getLogger(ImageGalleryModule.class.getName()); + private static final Logger logger = Logger.getLogger(ImageGalleryModule.class.getName()); private static final String MODULE_NAME = Bundle.ImageGalleryModule_moduleName(); + private static final Object controllerLock = new Object(); + private static ImageGalleryController controller; + + public static ImageGalleryController getController() throws NoCurrentCaseException { + synchronized (controllerLock) { + if (controller == null) { + try { + controller = new ImageGalleryController(Case.getCurrentCaseThrows()); + } catch (NoCurrentCaseException | TskCoreException ex) { + throw new NoCurrentCaseException("Error getting ImageGalleryController for the current case.", ex); + } + } + return controller; + } + } + + /** + * + * + * This method is invoked by virtue of the OnStart annotation on the OnStart + * class class + */ + static void onStart() { + Platform.setImplicitExit(false); + logger.info("Setting up ImageGallery listeners"); //NON-NLS + + IngestManager.getInstance().addIngestJobEventListener(new IngestJobEventListener()); + IngestManager.getInstance().addIngestModuleEventListener(new IngestModuleEventListener()); + Case.addPropertyChangeListener(new CaseEventListener()); + } + static String getModuleName() { return MODULE_NAME; } - /** * get the Path to the Case's ImageGallery ModuleOutput subfolder; ie * ".../[CaseName]/ModuleOutput/Image Gallery/" @@ -53,7 +98,7 @@ public class ImageGalleryModule { * * @return the Path to the ModuleOuput subfolder for Image Gallery */ - static Path getModuleOutputDir(Case theCase) { + public static Path getModuleOutputDir(Case theCase) { return Paths.get(theCase.getModuleDirectory(), getModuleName()); } @@ -83,19 +128,10 @@ public class ImageGalleryModule { * @return true if the drawable db is out of date for the given case, false * otherwise */ - public static boolean isDrawableDBStale(Case c) { - if (c != null) { - String stale = new PerCaseProperties(c).getConfigSetting(ImageGalleryModule.MODULE_NAME, PerCaseProperties.STALE); - return StringUtils.isNotBlank(stale) ? Boolean.valueOf(stale) : true; - } else { - return false; - } + public static boolean isDrawableDBStale(Case c) throws TskCoreException { + return new ImageGalleryController(c).isDataSourcesTableStale(); } - - - - /** * Is the given file 'supported' and not 'known'(nsrl hash hit). If so we * should include it in {@link DrawableDB} and UI @@ -105,7 +141,187 @@ public class ImageGalleryModule { * @return true if the given {@link AbstractFile} is "drawable" and not * 'known', else false */ - public static boolean isDrawableAndNotKnown(AbstractFile abstractFile) throws TskCoreException, FileTypeDetector.FileTypeDetectorInitException { + public static boolean isDrawableAndNotKnown(AbstractFile abstractFile) throws FileTypeDetector.FileTypeDetectorInitException { return (abstractFile.getKnown() != TskData.FileKnown.KNOWN) && FileTypeUtils.isDrawable(abstractFile); } + + /** + * Listener for IngestModuleEvents + */ + static private class IngestModuleEventListener implements PropertyChangeListener { + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (RuntimeProperties.runningWithGUI() == false) { + /* + * Running in "headless" mode, no need to process any events. + * This cannot be done earlier because the switch to core + * components inactive may not have been made at start up. + */ + IngestManager.getInstance().removeIngestModuleEventListener(this); + return; + } + + if (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName()) != FILE_DONE) { + return; + } + // getOldValue has fileID getNewValue has Abstractfile + AbstractFile file = (AbstractFile) evt.getNewValue(); + if (false == file.isFile()) { + return; + } + /* only process individual files in realtime on the node that is + * running the ingest. on a remote node, image files are processed + * enblock when ingest is complete */ + if (((AutopsyEvent) evt).getSourceType() != AutopsyEvent.SourceType.LOCAL) { + return; + } + + try { + ImageGalleryController con = getController(); + if (con.isListeningEnabled()) { + try { + if (isDrawableAndNotKnown(file)) { + //this file should be included and we don't already know about it from hash sets (NSRL) + con.queueDBTask(new ImageGalleryController.UpdateFileTask(file, controller.getDatabase())); + } else if (FileTypeUtils.getAllSupportedExtensions().contains(file.getNameExtension())) { + /* Doing this check results in fewer tasks queued + * up, and faster completion of db update. This file + * would have gotten scooped up in initial grab, but + * actually we don't need it */ + con.queueDBTask(new ImageGalleryController.RemoveFileTask(file, controller.getDatabase())); + } + + } catch (FileTypeDetector.FileTypeDetectorInitException ex) { + logger.log(Level.SEVERE, "Unable to determine if file is drawable and not known. Not making any changes to DB", ex); //NON-NLS + MessageNotifyUtil.Notify.error("Image Gallery Error", + "Unable to determine if file is drawable and not known. Not making any changes to DB. See the logs for details."); + } + } + } catch (NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS + } + } + } + + /** + * Listener for case events. + */ + static private class CaseEventListener implements PropertyChangeListener { + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (RuntimeProperties.runningWithGUI() == false) { + /* + * Running in "headless" mode, no need to process any events. + * This cannot be done earlier because the switch to core + * components inactive may not have been made at start up. + */ + Case.removePropertyChangeListener(this); + return; + } + ImageGalleryController con; + try { + con = getController(); + } catch (NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS + return; + } + switch (Case.Events.valueOf(evt.getPropertyName())) { + case CURRENT_CASE: + synchronized (controllerLock) { + // case has changes: close window, reset everything + SwingUtilities.invokeLater(ImageGalleryTopComponent::closeTopComponent); + if (controller != null) { + controller.reset(); + } + controller = null; + + Case newCase = (Case) evt.getNewValue(); + if (newCase != null) { + // a new case has been opened: connect db, groupmanager, start worker thread + try { + controller = new ImageGalleryController(newCase); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error changing case in ImageGallery.", ex); + } + } + } + break; + case DATA_SOURCE_ADDED: + //For a data source added on the local node, prepopulate all file data to drawable database + if (((AutopsyEvent) evt).getSourceType() == AutopsyEvent.SourceType.LOCAL) { + Content newDataSource = (Content) evt.getNewValue(); + if (con.isListeningEnabled()) { + con.queueDBTask(new ImageGalleryController.PrePopulateDataSourceFiles(newDataSource.getId(), controller)); + } + } + break; + case CONTENT_TAG_ADDED: + final ContentTagAddedEvent tagAddedEvent = (ContentTagAddedEvent) evt; + if (con.getDatabase().isInDB(tagAddedEvent.getAddedTag().getContent().getId())) { + con.getTagsManager().fireTagAddedEvent(tagAddedEvent); + } + break; + case CONTENT_TAG_DELETED: + final ContentTagDeletedEvent tagDeletedEvent = (ContentTagDeletedEvent) evt; + if (con.getDatabase().isInDB(tagDeletedEvent.getDeletedTagInfo().getContentID())) { + con.getTagsManager().fireTagDeletedEvent(tagDeletedEvent); + } + break; + default: + //we don't need to do anything for other events. + break; + } + } + } + + /** + * Listener for Ingest Job events. + */ + static private class IngestJobEventListener implements PropertyChangeListener { + + @NbBundle.Messages({ + "ImageGalleryController.dataSourceAnalyzed.confDlg.msg= A new data source was added and finished ingest.\n" + + "The image / video database may be out of date. " + + "Do you want to update the database with ingest results?\n", + "ImageGalleryController.dataSourceAnalyzed.confDlg.title=Image Gallery" + }) + @Override + public void propertyChange(PropertyChangeEvent evt) { + IngestJobEvent eventType = IngestJobEvent.valueOf(evt.getPropertyName()); + if (eventType != IngestJobEvent.DATA_SOURCE_ANALYSIS_COMPLETED + || ((AutopsyEvent) evt).getSourceType() != AutopsyEvent.SourceType.REMOTE) { + return; + } + // A remote node added a new data source and just finished ingest on it. + //drawable db is stale, and if ImageGallery is open, ask user what to do + ImageGalleryController con; + try { + con = getController(); + } catch (NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS + return; + } + con.setStale(true); + if (con.isListeningEnabled() && ImageGalleryTopComponent.isImageGalleryOpen()) { + SwingUtilities.invokeLater(() -> { + int showAnswer = JOptionPane.showConfirmDialog(ImageGalleryTopComponent.getTopComponent(), + Bundle.ImageGalleryController_dataSourceAnalyzed_confDlg_msg(), + Bundle.ImageGalleryController_dataSourceAnalyzed_confDlg_title(), + JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE); + + switch (showAnswer) { + case JOptionPane.YES_OPTION: + con.rebuildDB(); + break; + case JOptionPane.NO_OPTION: + case JOptionPane.CANCEL_OPTION: + default: + break; //do nothing + } + }); + } + } + } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryOptionsPanel.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryOptionsPanel.java index 7fbcaac1f0..80c35be23b 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryOptionsPanel.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryOptionsPanel.java @@ -18,13 +18,10 @@ */ package org.sleuthkit.autopsy.imagegallery; -import java.awt.event.ActionEvent; -import java.util.logging.Level; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.ingest.IngestManager; -import org.sleuthkit.autopsy.coreutils.Logger; /** * The Image/Video Gallery panel in the NetBeans provided Options Dialogs @@ -45,13 +42,8 @@ final class ImageGalleryOptionsPanel extends javax.swing.JPanel { enabledForCaseBox.setEnabled(Case.isCaseOpen() && IngestManager.getInstance().isIngestRunning() == false); }); - enabledByDefaultBox.addActionListener((ActionEvent e) -> { - controller.changed(); - }); - - enabledForCaseBox.addActionListener((ActionEvent e) -> { - controller.changed(); - }); + enabledByDefaultBox.addActionListener(actionEvent -> controller.changed()); + enabledForCaseBox.addActionListener(actionEvent -> controller.changed()); } /** @@ -204,19 +196,18 @@ final class ImageGalleryOptionsPanel extends javax.swing.JPanel { void store() { ImageGalleryPreferences.setEnabledByDefault(enabledByDefaultBox.isSelected()); - ImageGalleryController.getDefault().setListeningEnabled(enabledForCaseBox.isSelected()); + ImageGalleryPreferences.setGroupCategorizationWarningDisabled(groupCategorizationWarningBox.isSelected()); // If a case is open, save the per case setting try { Case openCase = Case.getCurrentCaseThrows(); + ImageGalleryModule.getController().setListeningEnabled(enabledForCaseBox.isSelected()); new PerCaseProperties(openCase).setConfigSetting(ImageGalleryModule.getModuleName(), PerCaseProperties.ENABLED, Boolean.toString(enabledForCaseBox.isSelected())); } catch (NoCurrentCaseException ex) { // It's not an error if there's no case open } - - - + } /** diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryPreferences.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryPreferences.java index 93329c3e62..37be779969 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryPreferences.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryPreferences.java @@ -46,8 +46,7 @@ public class ImageGalleryPreferences { * @return true if new cases should have image analyzer enabled. */ public static boolean isEnabledByDefault() { - final boolean aBoolean = preferences.getBoolean(ENABLED_BY_DEFAULT, true); - return aBoolean; + return preferences.getBoolean(ENABLED_BY_DEFAULT, true); } public static void setEnabledByDefault(boolean b) { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java index a6ce70c3ac..9263913748 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java @@ -18,27 +18,52 @@ */ package org.sleuthkit.autopsy.imagegallery; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.logging.Level; import java.util.stream.Collectors; import javafx.application.Platform; +import javafx.beans.Observable; import javafx.embed.swing.JFXPanel; +import javafx.geometry.Insets; +import javafx.scene.Node; import javafx.scene.Scene; +import javafx.scene.control.ChoiceDialog; +import javafx.scene.control.ProgressIndicator; import javafx.scene.control.SplitPane; import javafx.scene.control.TabPane; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.CornerRadii; import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.stage.Modality; +import javax.swing.SwingUtilities; +import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; +import static org.apache.commons.lang3.ObjectUtils.notEqual; import org.openide.explorer.ExplorerManager; import org.openide.explorer.ExplorerUtils; import org.openide.util.Lookup; +import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.openide.windows.Mode; import org.openide.windows.RetainLocation; import org.openide.windows.TopComponent; import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; +import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager; +import org.sleuthkit.autopsy.imagegallery.gui.GuiUtils; +import org.sleuthkit.autopsy.imagegallery.gui.NoGroupsDialog; import org.sleuthkit.autopsy.imagegallery.gui.StatusBar; import org.sleuthkit.autopsy.imagegallery.gui.SummaryTablePane; import org.sleuthkit.autopsy.imagegallery.gui.Toolbar; @@ -46,6 +71,9 @@ import org.sleuthkit.autopsy.imagegallery.gui.drawableviews.GroupPane; import org.sleuthkit.autopsy.imagegallery.gui.drawableviews.MetaDataPane; import org.sleuthkit.autopsy.imagegallery.gui.navpanel.GroupTree; import org.sleuthkit.autopsy.imagegallery.gui.navpanel.HashHitGroupList; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.TskCoreException; /** * Top component which displays ImageGallery interface. @@ -69,17 +97,17 @@ import org.sleuthkit.autopsy.imagegallery.gui.navpanel.HashHitGroupList; public final class ImageGalleryTopComponent extends TopComponent implements ExplorerManager.Provider, Lookup.Provider { public final static String PREFERRED_ID = "ImageGalleryTopComponent"; // NON-NLS - private static final Logger LOGGER = Logger.getLogger(ImageGalleryTopComponent.class.getName()); + private static final Logger logger = Logger.getLogger(ImageGalleryTopComponent.class.getName()); private static volatile boolean topComponentInitialized = false; private final ExplorerManager em = new ExplorerManager(); private final Lookup lookup = (ExplorerUtils.createLookup(em, getActionMap())); - private final ImageGalleryController controller = ImageGalleryController.getDefault(); + private ImageGalleryController controller; private SplitPane splitPane; private StackPane centralStack; - private BorderPane borderPane = new BorderPane(); + private final BorderPane borderPane = new BorderPane(); private StackPane fullUIStack; private MetaDataPane metaDataTable; private GroupPane groupPane; @@ -88,24 +116,97 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl private VBox leftPane; private Scene myScene; - public static void openTopComponent() { - //TODO:eventually move to this model, throwing away everything and rebuilding controller groupmanager etc for each case. - // synchronized (OpenTimelineAction.class) { - // if (timeLineController == null) { - // timeLineController = new TimeLineController(); - // LOGGER.log(Level.WARNING, "Failed to get TimeLineController from lookup. Instantiating one directly.S"); - // } - // } - // timeLineController.openTimeLine(); - final TopComponent tc = WindowManager.getDefault().findTopComponent(PREFERRED_ID); - if (tc != null) { - topComponentInitialized = true; - if (tc.isOpened() == false) { - tc.open(); - } - tc.toFront(); - tc.requestActive(); + private Node infoOverlay; + private final Region infoOverLayBackground = new TranslucentRegion(); + + /** + * Returns whether the ImageGallery window is open or not. + * + * @return true, if Image gallery is opened, false otherwise + */ + public static boolean isImageGalleryOpen() { + + final TopComponent topComponent = WindowManager.getDefault().findTopComponent(PREFERRED_ID); + if (topComponent != null) { + return topComponent.isOpened(); } + return false; + } + + /** + * Returns the top component window. + * + * @return Image gallery top component window, null if it's not open + */ + public static TopComponent getTopComponent() { + return WindowManager.getDefault().findTopComponent(PREFERRED_ID); + } + + @Messages({ + "ImageGalleryTopComponent.openTopCommponent.chooseDataSourceDialog.headerText=Choose a data source to view.", + "ImageGalleryTopComponent.openTopCommponent.chooseDataSourceDialog.contentText=Data source:", + "ImageGalleryTopComponent.openTopCommponent.chooseDataSourceDialog.all=All", + "ImageGalleryTopComponent.openTopCommponent.chooseDataSourceDialog.titleText=Image Gallery",}) + public static void openTopComponent() throws NoCurrentCaseException { + + final TopComponent topComponent = WindowManager.getDefault().findTopComponent(PREFERRED_ID); + if (topComponent == null) { + return; + } + topComponentInitialized = true; + if (topComponent.isOpened()) { + showTopComponent(topComponent); + return; + } + + List dataSources = Collections.emptyList(); + ImageGalleryController controller = ImageGalleryModule.getController(); + ((ImageGalleryTopComponent) topComponent).setController(controller); + try { + dataSources = controller.getSleuthKitCase().getDataSources(); + } catch (TskCoreException tskCoreException) { + logger.log(Level.SEVERE, "Unable to get data sourcecs.", tskCoreException); + } + GroupManager groupManager = controller.getGroupManager(); + synchronized (groupManager) { + if (dataSources.size() <= 1 + || groupManager.getGroupBy() != DrawableAttribute.PATH) { + /* if there is only one datasource or the grouping is already + * set to something other than path , don't both to ask for + * datasource */ + groupManager.regroup(null, groupManager.getGroupBy(), groupManager.getSortBy(), groupManager.getSortOrder(), true); + + showTopComponent(topComponent); + return; + } + } + + Map dataSourceNames = new HashMap<>(); + dataSourceNames.put("All", null); + dataSources.forEach(dataSource -> dataSourceNames.put(dataSource.getName(), dataSource)); + + Platform.runLater(() -> { + ChoiceDialog datasourceDialog = new ChoiceDialog<>(null, dataSourceNames.keySet()); + datasourceDialog.setTitle(Bundle.ImageGalleryTopComponent_openTopCommponent_chooseDataSourceDialog_titleText()); + datasourceDialog.setHeaderText(Bundle.ImageGalleryTopComponent_openTopCommponent_chooseDataSourceDialog_headerText()); + datasourceDialog.setContentText(Bundle.ImageGalleryTopComponent_openTopCommponent_chooseDataSourceDialog_contentText()); + datasourceDialog.initModality(Modality.APPLICATION_MODAL); + GuiUtils.setDialogIcons(datasourceDialog); + + Optional dataSourceName = datasourceDialog.showAndWait(); + DataSource dataSource = dataSourceName.map(dataSourceNames::get).orElse(null); + synchronized (groupManager) { + groupManager.regroup(dataSource, groupManager.getGroupBy(), groupManager.getSortBy(), groupManager.getSortOrder(), true); + } + SwingUtilities.invokeLater(() -> showTopComponent(topComponent)); + }); + } + + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + public static void showTopComponent(TopComponent topComponent) { + topComponent.open(); + topComponent.toFront(); + topComponent.requestActive(); } public static void closeTopComponent() { @@ -115,48 +216,57 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl try { etc.close(); } catch (Exception e) { - LOGGER.log(Level.SEVERE, "failed to close " + PREFERRED_ID, e); // NON-NLS + logger.log(Level.SEVERE, "failed to close " + PREFERRED_ID, e); // NON-NLS } } } } - public ImageGalleryTopComponent() { + public ImageGalleryTopComponent() throws NoCurrentCaseException { setName(Bundle.CTL_ImageGalleryTopComponent()); initComponents(); + setController(ImageGalleryModule.getController()); + } - Platform.runLater(() -> {//initialize jfx ui - fullUIStack = new StackPane(); //this is passed into controller - myScene = new Scene(fullUIStack); - jfxPanel.setScene(myScene); - groupPane = new GroupPane(controller); - centralStack = new StackPane(groupPane); //this is passed into controller - fullUIStack.getChildren().add(borderPane); - splitPane = new SplitPane(); - borderPane.setCenter(splitPane); - Toolbar toolbar = new Toolbar(controller); - borderPane.setTop(toolbar); - borderPane.setBottom(new StatusBar(controller)); + synchronized private void setController(ImageGalleryController controller) { + if (this.controller != null && notEqual(this.controller, controller)) { + this.controller.reset(); + } + this.controller = controller; + Platform.runLater(new Runnable() { + @Override + public void run() { + //initialize jfx ui + fullUIStack = new StackPane(); //this is passed into controller + myScene = new Scene(fullUIStack); + jfxPanel.setScene(myScene); + groupPane = new GroupPane(controller); + centralStack = new StackPane(groupPane); //this is passed into controller + fullUIStack.getChildren().add(borderPane); + splitPane = new SplitPane(); + borderPane.setCenter(splitPane); + Toolbar toolbar = new Toolbar(controller); + borderPane.setTop(toolbar); + borderPane.setBottom(new StatusBar(controller)); + metaDataTable = new MetaDataPane(controller); + groupTree = new GroupTree(controller); + hashHitList = new HashHitGroupList(controller); + TabPane tabPane = new TabPane(groupTree, hashHitList); + tabPane.setPrefWidth(TabPane.USE_COMPUTED_SIZE); + tabPane.setMinWidth(TabPane.USE_PREF_SIZE); + VBox.setVgrow(tabPane, Priority.ALWAYS); + leftPane = new VBox(tabPane, new SummaryTablePane(controller)); + SplitPane.setResizableWithParent(leftPane, Boolean.FALSE); + SplitPane.setResizableWithParent(groupPane, Boolean.TRUE); + SplitPane.setResizableWithParent(metaDataTable, Boolean.FALSE); + splitPane.getItems().addAll(leftPane, centralStack, metaDataTable); + splitPane.setDividerPositions(0.1, 1.0); - metaDataTable = new MetaDataPane(controller); + controller.regroupDisabledProperty().addListener((Observable observable) -> checkForGroups()); + controller.getGroupManager().getAnalyzedGroups().addListener((Observable observable) -> Platform.runLater(() -> checkForGroups())); - groupTree = new GroupTree(controller); - hashHitList = new HashHitGroupList(controller); - - TabPane tabPane = new TabPane(groupTree, hashHitList); - tabPane.setPrefWidth(TabPane.USE_COMPUTED_SIZE); - tabPane.setMinWidth(TabPane.USE_PREF_SIZE); - VBox.setVgrow(tabPane, Priority.ALWAYS); - leftPane = new VBox(tabPane, new SummaryTablePane(controller)); - SplitPane.setResizableWithParent(leftPane, Boolean.FALSE); - SplitPane.setResizableWithParent(groupPane, Boolean.TRUE); - SplitPane.setResizableWithParent(metaDataTable, Boolean.FALSE); - splitPane.getItems().addAll(leftPane, centralStack, metaDataTable); - splitPane.setDividerPositions(0.1, 1.0); - - ImageGalleryController.getDefault().setStacks(fullUIStack, centralStack); - ImageGalleryController.getDefault().setToolbar(toolbar); - ImageGalleryController.getDefault().setShowTree(() -> tabPane.getSelectionModel().select(groupTree)); + Platform.runLater(() -> checkForGroups()); + } }); } @@ -212,4 +322,100 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl public Lookup getLookup() { return lookup; } + + /** + * Check if there are any fully analyzed groups available from the + * GroupManager and remove blocking progress spinners if there are. If there + * aren't, add a blocking progress spinner with appropriate message. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + @NbBundle.Messages({ + "ImageGalleryController.noGroupsDlg.msg1=No groups are fully analyzed; but listening to ingest is disabled. " + + " No groups will be available until ingest is finished and listening is re-enabled.", + "ImageGalleryController.noGroupsDlg.msg2=No groups are fully analyzed yet, but ingest is still ongoing. Please Wait.", + "ImageGalleryController.noGroupsDlg.msg3=No groups are fully analyzed yet, but image / video data is still being populated. Please Wait.", + "ImageGalleryController.noGroupsDlg.msg4=There are no images/videos available from the added datasources; but listening to ingest is disabled. " + + " No groups will be available until ingest is finished and listening is re-enabled.", + "ImageGalleryController.noGroupsDlg.msg5=There are no images/videos in the added datasources.", + "ImageGalleryController.noGroupsDlg.msg6=There are no fully analyzed groups to display:" + + " the current Group By setting resulted in no groups, " + + "or no groups are fully analyzed but ingest is not running."}) + private void checkForGroups() { + GroupManager groupManager = controller.getGroupManager(); + synchronized (groupManager) { + if (isNotEmpty(groupManager.getAnalyzedGroups())) { + clearNotification(); + return; + } + + if (IngestManager.getInstance().isIngestRunning()) { + if (controller.isListeningEnabled()) { + replaceNotification(centralStack, + new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg2(), + new ProgressIndicator())); + } else { + replaceNotification(fullUIStack, + new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg1())); + } + return; + } + if (controller.getDBTasksQueueSizeProperty().get() > 0) { + replaceNotification(fullUIStack, + new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg3(), + new ProgressIndicator())); + return; + } + try { + if (controller.getDatabase().countAllFiles() <= 0) { + // there are no files in db + if (controller.isListeningEnabled()) { + replaceNotification(fullUIStack, + new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg5())); + } else { + replaceNotification(fullUIStack, + new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg4())); + } + return; + } + } catch (TskCoreException tskCoreException) { + logger.log(Level.SEVERE, "Error counting files in the database.", tskCoreException); + } + + if (false == groupManager.isRegrouping()) { + replaceNotification(centralStack, + new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg6())); + } + } + } + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private void replaceNotification(StackPane stackPane, Node newNode) { + clearNotification(); + infoOverlay = new StackPane(infoOverLayBackground, newNode); + if (stackPane != null) { + stackPane.getChildren().add(infoOverlay); + } + + } + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private void clearNotification() { + //remove the ingest spinner + fullUIStack.getChildren().remove(infoOverlay); + //remove the ingest spinner + centralStack.getChildren().remove(infoOverlay); + + } + + /** + * Region with partialy opacity used to block out parts of the UI behind a + * pseudo dialog. + */ + static final private class TranslucentRegion extends Region { + + TranslucentRegion() { + setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY))); + setOpacity(.4); + } + } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/OnStart.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/OnStart.java index b222b2bc7c..cf928514dd 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/OnStart.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/OnStart.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,26 +18,19 @@ */ package org.sleuthkit.autopsy.imagegallery; -import org.sleuthkit.autopsy.coreutils.Logger; - /** - * - * The {@link org.openide.modules.OnStart} annotation tells NetBeans to invoke - * this class's {@link OnStart#run()} method + * The org.openide.modules.OnStart annotation tells NetBeans to invoke this + * class's run method. */ @org.openide.modules.OnStart public class OnStart implements Runnable { - static private final Logger LOGGER = Logger.getLogger(OnStart.class.getName()); - /** - * - * - * This method is invoked by virtue of the {@link OnStart} annotation on the - * {@link ImageGalleryModule} class + * This method is invoked by virtue of the OnStart annotation on the this + * class */ @Override public void run() { - ImageGalleryController.getDefault().onStart(); + ImageGalleryModule.onStart(); } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/PerCaseProperties.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/PerCaseProperties.java index d87b13e880..56811352f0 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/PerCaseProperties.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/PerCaseProperties.java @@ -40,8 +40,6 @@ class PerCaseProperties { public static final String ENABLED = "enabled"; //NON-NLS - public static final String STALE = "stale"; //NON-NLS - private final Case theCase; PerCaseProperties(Case c) { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ThumbnailCache.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ThumbnailCache.java index 78b260e731..c6fdd893eb 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ThumbnailCache.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ThumbnailCache.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-15 Basis Technology Corp. + * Copyright 2013-18 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -54,9 +54,13 @@ import org.sleuthkit.datamodel.TskCoreException; * TODO: this was only a singleton for convenience, convert this to * non-singleton class -jm? */ -public enum ThumbnailCache { +public class ThumbnailCache { - instance; + private final ImageGalleryController controller; + + public ThumbnailCache(ImageGalleryController controller) { + this.controller = controller; + } private static final int MAX_THUMBNAIL_SIZE = 300; @@ -71,10 +75,6 @@ public enum ThumbnailCache { .softValues() .expireAfterAccess(10, TimeUnit.MINUTES).build(); - public static ThumbnailCache getDefault() { - return instance; - } - /** * currently desired icon size. is bound in {@link Toolbar} */ @@ -109,7 +109,7 @@ public enum ThumbnailCache { @Nullable public Image get(Long fileID) { try { - return get(ImageGalleryController.getDefault().getFileFromId(fileID)); + return get(controller.getFileFromID(fileID)); } catch (TskCoreException ex) { LOGGER.log(Level.WARNING, "Failed to load thumbnail for file: " + fileID, ex.getCause()); //NON-NLS return null; diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java index cd40efdb55..dbb27e81dc 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2017 Basis Technology Corp. + * Copyright 2013-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,7 +36,6 @@ import javax.swing.SwingWorker; import org.controlsfx.control.action.Action; import org.controlsfx.control.action.ActionUtils; import org.openide.util.NbBundle; -import org.openide.util.NbBundle.Messages; import org.openide.windows.TopComponent; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.actions.GetTagNameAndCommentDialog; @@ -50,8 +49,8 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager; import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.TagName; -import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; /** * Instances of this Action allow users to apply tags to content. @@ -75,14 +74,14 @@ public class AddTagAction extends Action { setEventHandler(actionEvent -> addTagWithComment("")); } - static public Menu getTagMenu(ImageGalleryController controller) { + static public Menu getTagMenu(ImageGalleryController controller) throws TskCoreException { return new TagMenu(controller); } private void addTagWithComment(String comment) { addTagsToFiles(tagName, comment, selectedFileIDs); } - + @NbBundle.Messages({"# {0} - fileID", "AddDrawableTagAction.addTagsToFiles.alert=Unable to tag file {0}."}) private void addTagsToFiles(TagName tagName, String comment, Set selectedFiles) { @@ -94,7 +93,7 @@ public class AddTagAction extends Action { DrawableTagsManager tagsManager = controller.getTagsManager(); for (Long fileID : selectedFiles) { try { - final DrawableFile file = controller.getFileFromId(fileID); + final DrawableFile file = controller.getFileFromID(fileID); LOGGER.log(Level.INFO, "tagging {0} with {1} and comment {2}", new Object[]{file.getName(), tagName.getDisplayName(), comment}); //NON-NLS List contentTags = tagsManager.getContentTags(file); @@ -141,7 +140,7 @@ public class AddTagAction extends Action { "AddDrawableTagAction.displayName.singular=Tag File"}) private static class TagMenu extends Menu { - TagMenu(ImageGalleryController controller) { + TagMenu(ImageGalleryController controller) throws TskCoreException { setGraphic(new ImageView(DrawableAttribute.TAGS.getIcon())); ObservableSet selectedFileIDs = controller.getSelectionModel().getSelected(); setText(selectedFileIDs.size() > 1 @@ -163,11 +162,10 @@ public class AddTagAction extends Action { empty.setDisable(true); quickTagMenu.getItems().add(empty); } else { - for (final TagName tagName : tagNames) { - AddTagAction addDrawableTagAction = new AddTagAction(controller, tagName, selectedFileIDs); - MenuItem tagNameItem = ActionUtils.createMenuItem(addDrawableTagAction); - quickTagMenu.getItems().add(tagNameItem); - } + tagNames.stream() + .map(tagName -> new AddTagAction(controller, tagName, selectedFileIDs)) + .map(ActionUtils::createMenuItem) + .forEachOrdered(quickTagMenu.getItems()::add); } /* diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java index 14944dff09..c6d540dbb3 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java @@ -138,7 +138,7 @@ public class CategorizeAction extends Action { TagName catZeroTagName = categoryManager.getTagName(DhsImageCategory.ZERO); for (long fileID : fileIDs) { try { - DrawableFile file = controller.getFileFromId(fileID); //drawable db access + DrawableFile file = controller.getFileFromID(fileID); //drawable db access if (createUndo) { DhsImageCategory oldCat = file.getCategory(); //drawable db access TagName oldCatTagName = categoryManager.getTagName(oldCat); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeGroupAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeGroupAction.java index f568fbd105..b49e1ca30c 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeGroupAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeGroupAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2015-16 Basis Technology Corp. + * Copyright 2015-18 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,11 +34,12 @@ import javafx.scene.control.Label; import javafx.scene.control.Separator; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import static org.apache.commons.lang.ObjectUtils.notEqual; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datamodel.DhsImageCategory; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryPreferences; -import org.sleuthkit.autopsy.datamodel.DhsImageCategory; import org.sleuthkit.datamodel.TskCoreException; /** @@ -52,32 +53,35 @@ public class CategorizeGroupAction extends CategorizeAction { public CategorizeGroupAction(DhsImageCategory newCat, ImageGalleryController controller) { super(controller, newCat, null); setEventHandler(actionEvent -> { - ObservableList fileIDs = controller.viewState().get().getGroup().getFileIDs(); + controller.getViewState().getGroup().ifPresent(group -> { + ObservableList fileIDs = group.getFileIDs(); - if (ImageGalleryPreferences.isGroupCategorizationWarningDisabled()) { - //if they have preveiously disabled the warning, just go ahead and apply categories. - addCatToFiles(ImmutableSet.copyOf(fileIDs)); - } else { - final Map catCountMap = new HashMap<>(); - - for (Long fileID : fileIDs) { - try { - DhsImageCategory category = controller.getFileFromId(fileID).getCategory(); - if (false == DhsImageCategory.ZERO.equals(category) && newCat.equals(category) == false) { - catCountMap.merge(category, 1L, Long::sum); - } - } catch (TskCoreException ex) { - LOGGER.log(Level.SEVERE, "Failed to categorize files.", ex); - } - } - - if (catCountMap.isEmpty()) { - //if there are not going to be any categories overwritten, skip the warning. + if (ImageGalleryPreferences.isGroupCategorizationWarningDisabled()) { + //if they have preveiously disabled the warning, just go ahead and apply categories. addCatToFiles(ImmutableSet.copyOf(fileIDs)); } else { - showConfirmationDialog(catCountMap, newCat, fileIDs); + final Map catCountMap = new HashMap<>(); + + for (Long fileID : fileIDs) { + try { + DhsImageCategory category = controller.getFileFromID(fileID).getCategory(); + if (false == DhsImageCategory.ZERO.equals(category) && newCat.equals(category) == false) { + catCountMap.merge(category, 1L, Long::sum); + } + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Failed to categorize files.", ex); + } + } + + if (catCountMap.isEmpty()) { + //if there are not going to be any categories overwritten, skip the warning. + addCatToFiles(ImmutableSet.copyOf(fileIDs)); + } else { + showConfirmationDialog(catCountMap, newCat, fileIDs); + } } - } + }); + }); } @@ -88,19 +92,18 @@ public class CategorizeGroupAction extends CategorizeAction { "CategorizeGroupAction.fileCountHeader=Files in the following categories will have their categories overwritten: "}) private void showConfirmationDialog(final Map catCountMap, DhsImageCategory newCat, ObservableList fileIDs) { - ButtonType categorizeButtonType = - new ButtonType(Bundle.CategorizeGroupAction_OverwriteButton_text(), ButtonBar.ButtonData.APPLY); + ButtonType categorizeButtonType + = new ButtonType(Bundle.CategorizeGroupAction_OverwriteButton_text(), ButtonBar.ButtonData.APPLY); VBox textFlow = new VBox(); for (Map.Entry entry : catCountMap.entrySet()) { - if (entry.getKey().equals(newCat) == false) { - if (entry.getValue() > 0) { - Label label = new Label(Bundle.CategorizeGroupAction_fileCountMessage(entry.getValue(), entry.getKey().getDisplayName()), - entry.getKey().getGraphic()); - label.setContentDisplay(ContentDisplay.RIGHT); - textFlow.getChildren().add(label); - } + if (entry.getValue() > 0 + && notEqual(entry.getKey(), newCat)) { + Label label = new Label(Bundle.CategorizeGroupAction_fileCountMessage(entry.getValue(), entry.getKey().getDisplayName()), + entry.getKey().getGraphic()); + label.setContentDisplay(ContentDisplay.RIGHT); + textFlow.getChildren().add(label); } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java index fe12dfe010..b53c36c91e 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-17 Basis Technology Corp. + * Copyright 2011-18 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,15 +18,16 @@ */ package org.sleuthkit.autopsy.imagegallery.actions; +import com.google.common.util.concurrent.MoreExecutors; import java.util.Optional; +import javafx.application.Platform; import javafx.beans.Observable; -import javafx.beans.binding.ObjectExpression; import javafx.collections.ObservableList; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import org.apache.commons.collections4.CollectionUtils; import org.controlsfx.control.action.Action; import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager; @@ -36,58 +37,87 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; * Marks the currently displayed group as "seen" and advances to the next unseen * group */ -@NbBundle.Messages({"NextUnseenGroup.markGroupSeen=Mark Group Seen", - "NextUnseenGroup.nextUnseenGroup=Next Unseen group"}) +@NbBundle.Messages({ + "NextUnseenGroup.markGroupSeen=Mark Group Seen", + "NextUnseenGroup.nextUnseenGroup=Next Unseen group", + "NextUnseenGroup.allGroupsSeen=All Groups Have Been Seen"}) public class NextUnseenGroup extends Action { - private static final Image END = - new Image(NextUnseenGroup.class.getResourceAsStream("/org/sleuthkit/autopsy/imagegallery/images/control-stop.png")); //NON-NLS - private static final Image ADVANCE = - new Image(NextUnseenGroup.class.getResourceAsStream("/org/sleuthkit/autopsy/imagegallery/images/control-double.png")); //NON-NLS + private static final String IMAGE_PATH = "/org/sleuthkit/autopsy/imagegallery/images/"; //NON-NLS + private static final Image END_IMAGE = new Image(NextUnseenGroup.class.getResourceAsStream( + IMAGE_PATH + "control-stop.png")); //NON-NLS + private static final Image ADVANCE_IMAGE = new Image(NextUnseenGroup.class.getResourceAsStream( + IMAGE_PATH + "control-double.png")); //NON-NLS private static final String MARK_GROUP_SEEN = Bundle.NextUnseenGroup_markGroupSeen(); private static final String NEXT_UNSEEN_GROUP = Bundle.NextUnseenGroup_nextUnseenGroup(); + private static final String ALL_GROUPS_SEEN = Bundle.NextUnseenGroup_allGroupsSeen(); - private final GroupManager groupManager; + private final ImageGalleryController controller; private final ObservableList unSeenGroups; - private final ObservableList analyzedGroups; + private final GroupManager groupManager; public NextUnseenGroup(ImageGalleryController controller) { super(NEXT_UNSEEN_GROUP); + setGraphic(new ImageView(ADVANCE_IMAGE)); + + this.controller = controller; groupManager = controller.getGroupManager(); unSeenGroups = groupManager.getUnSeenGroups(); - analyzedGroups = groupManager.getAnalyzedGroups(); - setGraphic(new ImageView(ADVANCE)); + unSeenGroups.addListener((Observable observable) -> updateButton()); + controller.viewStateProperty().addListener((Observable observable) -> updateButton()); - //TODO: do we need both these listeners? - analyzedGroups.addListener((Observable o) -> this.updateButton()); - unSeenGroups.addListener((Observable o) -> this.updateButton()); - - setEventHandler(event -> { - //fx-thread + setEventHandler(event -> { //on fx-thread //if there is a group assigned to the view, mark it as seen - Optional.ofNullable(controller.viewState()) - .map(ObjectExpression::getValue) - .map(GroupViewState::getGroup) - .ifPresent(group -> groupManager.markGroupSeen(group, true)); - - if (unSeenGroups.isEmpty() == false) { - controller.advance(GroupViewState.tile(unSeenGroups.get(0)), true); - updateButton(); - } + Optional.ofNullable(controller.getViewState()) + .flatMap(GroupViewState::getGroup) + .ifPresent(group -> { + groupManager.markGroupSeen(group, true) + .addListener(this::advanceToNextUnseenGroup, MoreExecutors.newDirectExecutorService()); + }); }); updateButton(); } - @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private void advanceToNextUnseenGroup() { + synchronized (groupManager) { + if (CollectionUtils.isNotEmpty(unSeenGroups)) { + controller.advance(GroupViewState.tile(unSeenGroups.get(0))); + } + + updateButton(); + } + } + private void updateButton() { - setDisabled(unSeenGroups.isEmpty()); - if (unSeenGroups.size() <= 1) { - setText(MARK_GROUP_SEEN); - setGraphic(new ImageView(END)); + int size = unSeenGroups.size(); + + if (size < 1) { + //there are no unseen groups. + Platform.runLater(() -> { + setDisabled(true); + setText(ALL_GROUPS_SEEN); + setGraphic(null); + }); } else { - setText(NEXT_UNSEEN_GROUP); - setGraphic(new ImageView(ADVANCE)); + DrawableGroup get = unSeenGroups.get(0); + DrawableGroup orElse = Optional.ofNullable(controller.getViewState()).flatMap(GroupViewState::getGroup).orElse(null); + boolean equals = get.equals(orElse); + if (size == 1 & equals) { + //The only unseen group is the one that is being viewed. + Platform.runLater(() -> { + setDisabled(false); + setText(MARK_GROUP_SEEN); + setGraphic(new ImageView(END_IMAGE)); + }); + } else { + //there are more unseen groups. + Platform.runLater(() -> { + setDisabled(false); + setText(NEXT_UNSEEN_GROUP); + setGraphic(new ImageView(ADVANCE_IMAGE)); + }); + } } } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java index e10a679792..efbdc3de00 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java @@ -21,16 +21,11 @@ package org.sleuthkit.autopsy.imagegallery.actions; import java.awt.Component; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; -import java.io.IOException; -import java.net.URL; import java.util.logging.Level; import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; -import javafx.scene.control.Dialog; -import javafx.scene.image.Image; import javafx.stage.Modality; -import javafx.stage.Stage; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JMenuItem; @@ -49,42 +44,27 @@ import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.core.Installer; import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryModule; import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent; +import org.sleuthkit.autopsy.imagegallery.gui.GuiUtils; import org.sleuthkit.datamodel.TskCoreException; @ActionID(category = "Tools", id = "org.sleuthkit.autopsy.imagegallery.OpenAction") @ActionReferences(value = { - @ActionReference(path = "Menu/Tools", position = 101), + @ActionReference(path = "Menu/Tools", position = 101) + , @ActionReference(path = "Toolbars/Case", position = 101)}) @ActionRegistration(displayName = "#CTL_OpenAction", lazy = false) @Messages({"CTL_OpenAction=Images/Videos", - "OpenAction.stale.confDlg.msg=The image / video database may be out of date. " + - "Do you want to update and listen for further ingest results?\n" + - "Choosing 'yes' will update the database and enable listening to future ingests.", + "OpenAction.stale.confDlg.msg=The image / video database may be out of date. " + + "Do you want to update and listen for further ingest results?\n" + + "Choosing 'yes' will update the database and enable listening to future ingests.", "OpenAction.stale.confDlg.title=Image Gallery"}) public final class OpenAction extends CallableSystemAction { private static final Logger logger = Logger.getLogger(OpenAction.class.getName()); private static final String VIEW_IMAGES_VIDEOS = Bundle.CTL_OpenAction(); - - /** - * Image to use as title bar icon in dialogs - */ - private static final Image AUTOPSY_ICON; - - static { - Image tempImg = null; - try { - tempImg = new Image(new URL("nbresloc:/org/netbeans/core/startup/frame.gif").openStream()); //NON-NLS - } catch (IOException ex) { - logger.log(Level.WARNING, "Failed to load branded icon for progress dialog.", ex); //NON-NLS - } - AUTOPSY_ICON = tempImg; - } - private static final long FILE_LIMIT = 6_000_000; private final PropertyChangeListener pcl; @@ -145,10 +125,7 @@ public final class OpenAction extends CallableSystemAction { } @Override - @SuppressWarnings("fallthrough") - @NbBundle.Messages({ - "OpenAction.dialogTitle=Image Gallery" - }) + @NbBundle.Messages({"OpenAction.dialogTitle=Image Gallery"}) public void performAction() { //check case @@ -165,24 +142,49 @@ public final class OpenAction extends CallableSystemAction { setEnabled(false); return; } - if (ImageGalleryModule.isDrawableDBStale(currentCase)) { - //drawable db is stale, ask what to do - int answer = JOptionPane.showConfirmDialog(WindowManager.getDefault().getMainWindow(), Bundle.OpenAction_stale_confDlg_msg(), - Bundle.OpenAction_stale_confDlg_title(), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE); + try { + ImageGalleryController controller = ImageGalleryModule.getController(); + if (controller.isDataSourcesTableStale()) { + //drawable db is stale, ask what to do + int answer = JOptionPane.showConfirmDialog( + WindowManager.getDefault().getMainWindow(), + Bundle.OpenAction_stale_confDlg_msg(), + Bundle.OpenAction_stale_confDlg_title(), + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE); - switch (answer) { - case JOptionPane.YES_OPTION: - ImageGalleryController.getDefault().setListeningEnabled(true); - //fall through - case JOptionPane.NO_OPTION: - ImageGalleryTopComponent.openTopComponent(); + switch (answer) { + case JOptionPane.YES_OPTION: + /* For a single-user case, we favor user experience, and + * rebuild the database as soon as Image Gallery is + * enabled for the case. For a multi-user case, we favor + * overall performance and user experience, not every + * user may want to review images, so we rebuild the + * database only when a user launches Image Gallery. + */ + + + if (currentCase.getCaseType() == Case.CaseType.SINGLE_USER_CASE) { + controller.setListeningEnabled(true); + } else { + controller.rebuildDB(); + } + ImageGalleryTopComponent.openTopComponent(); + break; + + case JOptionPane.NO_OPTION: { + ImageGalleryTopComponent.openTopComponent(); + } break; - case JOptionPane.CANCEL_OPTION: - break; //do nothing + case JOptionPane.CANCEL_OPTION: + break; //do nothing + } + } else { + //drawable db is not stale, just open it + ImageGalleryTopComponent.openTopComponent(); } - } else { - //drawable db is not stale, just open it - ImageGalleryTopComponent.openTopComponent(); + } catch (NoCurrentCaseException noCurrentCaseException) { + logger.log(Level.WARNING, "There was no case open when Image Gallery was opened.", noCurrentCaseException); } } @@ -198,16 +200,6 @@ public final class OpenAction extends CallableSystemAction { return false; } - /** - * Set the title bar icon for the given Dialog to be the Autopsy logo icon. - * - * @param dialog The dialog to set the title bar icon for. - */ - @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - private static void setDialogIcons(Dialog dialog) { - ((Stage) dialog.getDialogPane().getScene().getWindow()).getIcons().setAll(AUTOPSY_ICON); - } - @NbBundle.Messages({ "ImageGallery.showTooManyFiles.contentText=" + "There are too many files in the DB to ensure reasonable performance." @@ -218,7 +210,7 @@ public final class OpenAction extends CallableSystemAction { Bundle.ImageGallery_showTooManyFiles_contentText(), ButtonType.OK); dialog.initModality(Modality.APPLICATION_MODAL); dialog.setTitle(Bundle.OpenAction_dialogTitle()); - setDialogIcons(dialog); + GuiUtils.setDialogIcons(dialog); dialog.setHeaderText(Bundle.ImageGallery_showTooManyFiles_headerText()); dialog.showAndWait(); } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagGroupAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagGroupAction.java index afd6a7dcfc..d48349ac22 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagGroupAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagGroupAction.java @@ -29,9 +29,10 @@ public class TagGroupAction extends AddTagAction { public TagGroupAction(final TagName tagName, ImageGalleryController controller) { super(controller, tagName, null); - setEventHandler(actionEvent -> - new AddTagAction(controller, tagName, ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs())). - handle(actionEvent) - ); + setEventHandler(actionEvent -> { + controller.getViewState().getGroup().ifPresent(group -> { + new AddTagAction(controller, tagName, ImmutableSet.copyOf(group.getFileIDs())).handle(actionEvent); + }); + }); } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java index 66aa6c578d..5168e25630 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2015-16 Basis Technology Corp. + * Copyright 2015-18 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,20 +34,19 @@ import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.datamodel.DhsImageCategory; +import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; - /** * Provides a cached view of the number of files per category, and fires - * {@link CategoryChangeEvent}s when files are categorized. + * CategoryChangeEvents when files are categorized. * * To receive CategoryChangeEvents, a listener must register itself, and - * implement a public method annotated with {@link Subscribe} that accepts one - * argument of type CategoryChangeEvent + * implement a public method annotated with Subscribe that accepts one argument + * of type CategoryChangeEvent * * TODO: currently these two functions (cached counts and events) are separate * although they are related. Can they be integrated more? @@ -64,14 +63,14 @@ public class CategoryManager { * initialized from this, and the counting of CAT-0 is always delegated to * this db. */ - private DrawableDB db; + private final DrawableDB drawableDb; /** - * Used to distribute {@link CategoryChangeEvent}s + * Used to distribute CategoryChangeEvents */ private final EventBus categoryEventBus = new AsyncEventBus(Executors.newSingleThreadExecutor( - new BasicThreadFactory.Builder().namingPattern("Category Event Bus").uncaughtExceptionHandler((Thread t, Throwable e) -> { //NON-NLS - LOGGER.log(Level.SEVERE, "Uncaught exception in category event bus handler", e); //NON-NLS + new BasicThreadFactory.Builder().namingPattern("Category Event Bus").uncaughtExceptionHandler((Thread thread, Throwable throwable) -> { //NON-NLS + LOGGER.log(Level.SEVERE, "Uncaught exception in category event bus handler", throwable); //NON-NLS }).build() )); @@ -80,38 +79,29 @@ public class CategoryManager { * the count related methods go through this cache, which loads initial * values from the database if needed. */ - private final LoadingCache categoryCounts = - CacheBuilder.newBuilder().build(CacheLoader.from(this::getCategoryCountHelper)); + private final LoadingCache categoryCounts + = CacheBuilder.newBuilder().build(CacheLoader.from(this::getCategoryCountHelper)); /** * cached TagNames corresponding to Categories, looked up from * autopsyTagManager at initial request or if invalidated by case change. */ - private final LoadingCache catTagNameMap = - CacheBuilder.newBuilder().build(CacheLoader.from( - cat -> getController().getTagsManager().getTagName(cat) - )); + private final LoadingCache catTagNameMap + = CacheBuilder.newBuilder().build(new CacheLoader() { + @Override + public TagName load(DhsImageCategory cat) throws TskCoreException { + return getController().getTagsManager().getTagName(cat); + } + }); public CategoryManager(ImageGalleryController controller) { this.controller = controller; + this.drawableDb = controller.getDatabase(); } private ImageGalleryController getController() { return controller; } - /** - * assign a new db. the counts cache is invalidated and all subsequent db - * lookups go to the new db. - * - * Also clears the Category TagNames - * - * @param db - */ - synchronized public void setDb(DrawableDB db) { - this.db = db; - invalidateCaches(); - } - synchronized public void invalidateCaches() { categoryCounts.invalidateAll(); catTagNameMap.invalidateAll(); @@ -131,7 +121,7 @@ public class CategoryManager { // is going on, so always use the list of file IDs we already have along with the // other category counts instead of trying to track it separately. long allOtherCatCount = getCategoryCount(DhsImageCategory.ONE) + getCategoryCount(DhsImageCategory.TWO) + getCategoryCount(DhsImageCategory.THREE) + getCategoryCount(DhsImageCategory.FOUR) + getCategoryCount(DhsImageCategory.FIVE); - return db.getNumberOfImageFilesInList() - allOtherCatCount; + return drawableDb.getNumberOfImageFilesInList() - allOtherCatCount; } else { return categoryCounts.getUnchecked(cat).sum(); } @@ -151,7 +141,7 @@ public class CategoryManager { /** * decrement the cached value for the number of files with the given - * {@link DhsImageCategory} + * DhsImageCategory * * @param cat the Category to decrement */ @@ -175,7 +165,7 @@ public class CategoryManager { LongAdder longAdder = new LongAdder(); longAdder.decrement(); try { - longAdder.add(db.getCategoryCount(cat)); + longAdder.add(drawableDb.getCategoryCount(cat)); longAdder.increment(); } catch (IllegalStateException ex) { LOGGER.log(Level.WARNING, "Case closed while getting files"); //NON-NLS @@ -212,15 +202,14 @@ public class CategoryManager { try { categoryEventBus.unregister(listener); } catch (IllegalArgumentException e) { - if (e.getMessage().contains("missing event subscriber for an annotated method. Is " + listener + " registered?")) { //NON-NLS - /* - * We don't fully understand why we are getting this exception - * when the groups should all be registered. To avoid cluttering - * the logs we have disabled recording this exception. This - * documented in issues 738 and 802. - */ - //LOGGER.log(Level.WARNING, "Attempted to unregister {0} for category change events, but it was not registered.", listener.toString()); //NON-NLS - } else { + /* + * We don't fully understand why we are getting this exception when + * the groups should all be registered. To avoid cluttering the logs + * we have disabled recording this exception. This documented in + * issues 738 and 802. + */ + + if (!e.getMessage().contains("missing event subscriber for an annotated method. Is " + listener + " registered?")) { //NON-NLS throw e; } } @@ -258,7 +247,7 @@ public class CategoryManager { //remove old category tag(s) if necessary for (ContentTag ct : tagsManager.getContentTags(addedTag.getContent())) { if (ct.getId() != addedTag.getId() - && CategoryManager.isCategoryTagName(ct.getName())) { + && CategoryManager.isCategoryTagName(ct.getName())) { try { tagsManager.deleteContentTag(ct); } catch (TskCoreException tskException) { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java index 7688ccd146..f779978dfb 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-16 Basis Technology Corp. + * Copyright 2013-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,8 @@ */ package org.sleuthkit.autopsy.imagegallery.datamodel; -import org.sleuthkit.autopsy.datamodel.DhsImageCategory; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -38,7 +39,9 @@ import java.util.Map; import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; @@ -46,9 +49,11 @@ import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.concurrent.GuardedBy; import javax.swing.SortOrder; +import static org.apache.commons.lang3.ObjectUtils.notEqual; import org.apache.commons.lang3.StringUtils; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datamodel.DhsImageCategory; import org.sleuthkit.autopsy.imagegallery.FileTypeUtils; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryModule; @@ -59,24 +64,26 @@ import static org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupSortBy. import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.CaseDbAccessManager.CaseDbAccessQueryCallback; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.SleuthkitCase.CaseDbTransaction; import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData.DbType; +import org.sleuthkit.datamodel.TskDataException; import org.sqlite.SQLiteJDBCLoader; /** - * This class is the public interface to the Image / Video Analyzer SQLite - * database. This class borrows a lot of ideas and techniques (for good or ill) - * from {@link SleuthkitCase}. - * - * TODO: Creating an abstract base class for sqlite databases may make sense in - * the future. see also {@link EventsDB} in the timeline viewer. + * This class is the public interface to the Image Gallery SQLite database. This + * class borrows a lot of ideas and techniques (for good or ill) from + * SleuthkitCase */ public final class DrawableDB { - private static final org.sleuthkit.autopsy.coreutils.Logger LOGGER = Logger.getLogger(DrawableDB.class.getName()); + private static final Logger logger = Logger.getLogger(DrawableDB.class.getName()); //column name constants////////////////////// private static final String ANALYZED = "analyzed"; //NON-NLS @@ -85,24 +92,23 @@ public final class DrawableDB { private static final String HASH_SET_NAME = "hash_set_name"; //NON-NLS + private static final String GROUPS_TABLENAME = "image_gallery_groups"; //NON-NLS + private static final String GROUPS_SEEN_TABLENAME = "image_gallery_groups_seen"; //NON-NLS + private final PreparedStatement insertHashSetStmt; - private final PreparedStatement groupSeenQueryStmt; - - private final PreparedStatement insertGroupStmt; - private final List preparedStatements = new ArrayList<>(); private final PreparedStatement removeFileStmt; - private final PreparedStatement updateGroupStmt; - private final PreparedStatement selectHashSetStmt; private final PreparedStatement selectHashSetNamesStmt; private final PreparedStatement insertHashHitStmt; + private final PreparedStatement updateDataSourceStmt; + private final PreparedStatement updateFileStmt; private final PreparedStatement insertFileStmt; @@ -122,11 +128,14 @@ public final class DrawableDB { private final PreparedStatement hashSetGroupStmt; + private final PreparedStatement pathGroupFilterByDataSrcStmt; + /** - * map from {@link DrawableAttribute} to the {@link PreparedStatement} thet + * map from {@link DrawableAttribute} to the {@link PreparedStatement} that * is used to select groups for that attribute */ private final Map, PreparedStatement> groupStatementMap = new HashMap<>(); + private final Map, PreparedStatement> groupStatementFilterByDataSrcMap = new HashMap<>(); private final GroupManager groupManager; @@ -142,12 +151,21 @@ public final class DrawableDB { try { Class.forName("org.sqlite.JDBC"); } catch (ClassNotFoundException ex) { - LOGGER.log(Level.SEVERE, "Failed to load sqlite JDBC driver", ex); //NON-NLS + logger.log(Level.SEVERE, "Failed to load sqlite JDBC driver", ex); //NON-NLS } } private final SleuthkitCase tskCase; private final ImageGalleryController controller; + /** + * Enum to track Image gallery db rebuild status for a data source + */ + public enum DrawableDbBuildStatusEnum { + UNKNOWN, /// no known status + IN_PROGRESS, /// drawable db rebuild has been started for the data source + COMPLETE; /// drawable db rebuild is complete for the data source + } + //////////////general database logic , mostly borrowed from sleuthkitcase /** * Lock to protect against concurrent write accesses to case database and to @@ -195,7 +213,7 @@ public final class DrawableDB { * * @throws SQLException if there is problem creating or configuring the db */ - private DrawableDB(Path dbPath, ImageGalleryController controller) throws SQLException, ExceptionInInitializerError, IOException { + private DrawableDB(Path dbPath, ImageGalleryController controller) throws TskCoreException, SQLException, IOException { this.dbPath = dbPath; this.controller = controller; this.tskCase = controller.getSleuthKitCase(); @@ -203,11 +221,15 @@ public final class DrawableDB { Files.createDirectories(dbPath.getParent()); if (initializeDBSchema()) { updateFileStmt = prepareStatement( - "INSERT OR REPLACE INTO drawable_files (obj_id , path, name, created_time, modified_time, make, model, analyzed) " //NON-NLS - + "VALUES (?,?,?,?,?,?,?,?)"); //NON-NLS + "INSERT OR REPLACE INTO drawable_files (obj_id, data_source_obj_id, path, name, created_time, modified_time, make, model, analyzed) " //NON-NLS + + "VALUES (?,?,?,?,?,?,?,?,?)"); //NON-NLS insertFileStmt = prepareStatement( - "INSERT OR IGNORE INTO drawable_files (obj_id , path, name, created_time, modified_time, make, model, analyzed) " //NON-NLS - + "VALUES (?,?,?,?,?,?,?,?)"); //NON-NLS + "INSERT OR IGNORE INTO drawable_files (obj_id, data_source_obj_id, path, name, created_time, modified_time, make, model, analyzed) " //NON-NLS + + "VALUES (?,?,?,?,?,?,?,?,?)"); //NON-NLS + + updateDataSourceStmt = prepareStatement( + "INSERT OR REPLACE INTO datasources (ds_obj_id, drawable_db_build_status) " //NON-NLS + + " VALUES (?,?)"); //NON-NLS removeFileStmt = prepareStatement("DELETE FROM drawable_files WHERE obj_id = ?"); //NON-NLS @@ -220,10 +242,8 @@ public final class DrawableDB { analyzedGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE analyzed = ?", DrawableAttribute.ANALYZED); //NON-NLS hashSetGroupStmt = prepareStatement("SELECT drawable_files.obj_id AS obj_id, analyzed FROM drawable_files , hash_sets , hash_set_hits WHERE drawable_files.obj_id = hash_set_hits.obj_id AND hash_sets.hash_set_id = hash_set_hits.hash_set_id AND hash_sets.hash_set_name = ?", DrawableAttribute.HASHSET); //NON-NLS - updateGroupStmt = prepareStatement("insert or replace into groups (seen, value, attribute) values( ?, ? , ?)"); //NON-NLS - insertGroupStmt = prepareStatement("insert or ignore into groups (value, attribute) values (?,?)"); //NON-NLS - - groupSeenQueryStmt = prepareStatement("SELECT seen FROM groups WHERE value = ? AND attribute = ?"); //NON-NLS + //add other xyzFilterByDataSrc prepared statments as we add support for filtering by DS to other groups + pathGroupFilterByDataSrcStmt = prepareFilterByDataSrcStatement("SELECT obj_id , analyzed FROM drawable_files WHERE path = ? AND data_source_obj_id = ?", DrawableAttribute.PATH); selectHashSetNamesStmt = prepareStatement("SELECT DISTINCT hash_set_name FROM hash_sets"); //NON-NLS insertHashSetStmt = prepareStatement("INSERT OR IGNORE INTO hash_sets (hash_set_name) VALUES (?)"); //NON-NLS @@ -231,14 +251,28 @@ public final class DrawableDB { insertHashHitStmt = prepareStatement("INSERT OR IGNORE INTO hash_set_hits (hash_set_id, obj_id) VALUES (?,?)"); //NON-NLS - for (DhsImageCategory cat : DhsImageCategory.values()) { - insertGroup(cat.getDisplayName(), DrawableAttribute.CATEGORY); + CaseDbTransaction caseDbTransaction = null; + try { + caseDbTransaction = tskCase.beginTransaction(); + for (DhsImageCategory cat : DhsImageCategory.values()) { + insertGroup(cat.getDisplayName(), DrawableAttribute.CATEGORY, caseDbTransaction); + } + caseDbTransaction.commit(); + } catch (TskCoreException ex) { + if (null != caseDbTransaction) { + try { + caseDbTransaction.rollback(); + } catch (TskCoreException ex2) { + logger.log(Level.SEVERE, "Error in trying to rollback transaction", ex2); + } + } + throw ex; } + initializeImageList(); } else { - throw new ExceptionInInitializerError(); + throw new TskCoreException("Failed to initialize Image Gallery db schema"); } - } /** @@ -281,23 +315,55 @@ public final class DrawableDB { } /** - * public factory method. Creates and opens a connection to a new database * - * at the given path. + * calls {@link DrawableDB#prepareStatement(java.lang.String) , + * and then add the statement to the groupStatementFilterByDataSrcMap map used to lookup + * statements by the attribute/column they group on * - * @param dbPath + * @param stmtString the string representation of the sqlite statement to + * prepare + * @param attr the {@link DrawableAttribute} this query groups by + * * + * @return the prepared statement * - * @return + * @throws SQLExceptionif unable to prepare the statement */ - public static DrawableDB getDrawableDB(Path dbPath, ImageGalleryController controller) { + private PreparedStatement prepareFilterByDataSrcStatement(String stmtString, DrawableAttribute attr) throws SQLException { + PreparedStatement prepareStatement = prepareStatement(stmtString); + if (attr != null) { + groupStatementFilterByDataSrcMap.put(attr, prepareStatement); + } + return prepareStatement; + } + + private void setQueryParams(PreparedStatement statement, GroupKey groupKey) throws SQLException { + + statement.setObject(1, groupKey.getValue()); + + if (groupKey.getDataSource().isPresent() + && (groupKey.getAttribute() == DrawableAttribute.PATH)) { + statement.setObject(2, groupKey.getDataSourceObjId()); + } + } + + /** + * public factory method. Creates and opens a connection to a new database * + * at the given path. * + * + * @param controller + * + * @return A DrawableDB for the given controller. + * + * @throws org.sleuthkit.datamodel.TskCoreException + */ + public static DrawableDB getDrawableDB(ImageGalleryController controller) throws TskCoreException { + Path dbPath = ImageGalleryModule.getModuleOutputDir(controller.getAutopsyCase()); try { return new DrawableDB(dbPath.resolve("drawable.db"), controller); //NON-NLS } catch (SQLException ex) { - LOGGER.log(Level.SEVERE, "sql error creating database connection", ex); //NON-NLS - return null; - } catch (ExceptionInInitializerError | IOException ex) { - LOGGER.log(Level.SEVERE, "error creating database connection", ex); //NON-NLS - return null; + throw new TskCoreException("sql error creating database connection", ex); //NON-NLS + } catch (IOException ex) { + throw new TskCoreException("Error creating database connection", ex); //NON-NLS } } @@ -328,11 +394,11 @@ public final class DrawableDB { } try { - LOGGER.log(Level.INFO, String.format("sqlite-jdbc version %s loaded in %s mode", //NON-NLS + logger.log(Level.INFO, String.format("sqlite-jdbc version %s loaded in %s mode", //NON-NLS SQLiteJDBCLoader.getVersion(), SQLiteJDBCLoader.isNativeMode() - ? "native" : "pure-java")); //NON-NLS + ? "native" : "pure-java")); //NON-NLS } catch (Exception exception) { - LOGGER.log(Level.WARNING, "exception while checking sqlite-jdbc version and mode", exception); //NON-NLS + logger.log(Level.WARNING, "exception while checking sqlite-jdbc version and mode", exception); //NON-NLS } } @@ -351,56 +417,92 @@ public final class DrawableDB { setPragmas(); } catch (SQLException ex) { - LOGGER.log(Level.SEVERE, "problem accessing database", ex); //NON-NLS - return false; - } - try (Statement stmt = con.createStatement()) { - String sql = "CREATE TABLE if not exists drawable_files " //NON-NLS - + "( obj_id INTEGER PRIMARY KEY, " //NON-NLS - + " path VARCHAR(255), " //NON-NLS - + " name VARCHAR(255), " //NON-NLS - + " created_time integer, " //NON-NLS - + " modified_time integer, " //NON-NLS - + " make VARCHAR(255), " //NON-NLS - + " model VARCHAR(255), " //NON-NLS - + " analyzed integer DEFAULT 0)"; //NON-NLS - stmt.execute(sql); - } catch (SQLException ex) { - LOGGER.log(Level.SEVERE, "problem creating drawable_files table", ex); //NON-NLS + logger.log(Level.SEVERE, "problem accessing database", ex); //NON-NLS return false; } try (Statement stmt = con.createStatement()) { - String sql = "CREATE TABLE if not exists groups " //NON-NLS - + "(group_id INTEGER PRIMARY KEY, " //NON-NLS - + " value VARCHAR(255) not null, " //NON-NLS - + " attribute VARCHAR(255) not null, " //NON-NLS - + " seen integer DEFAULT 0, " //NON-NLS - + " UNIQUE(value, attribute) )"; //NON-NLS + String sql = "CREATE TABLE IF NOT EXISTS datasources " //NON-NLS + + "( id INTEGER PRIMARY KEY, " //NON-NLS + + " ds_obj_id integer UNIQUE NOT NULL, " + + " drawable_db_build_status VARCHAR(128) )"; //NON-NLS stmt.execute(sql); } catch (SQLException ex) { - LOGGER.log(Level.SEVERE, "problem creating groups table", ex); //NON-NLS + logger.log(Level.SEVERE, "problem creating datasources table", ex); //NON-NLS + return false; + } + + try (Statement stmt = con.createStatement()) { + String sql = "CREATE TABLE if not exists drawable_files " //NON-NLS + + "( obj_id INTEGER PRIMARY KEY, " //NON-NLS + + " data_source_obj_id INTEGER NOT NULL, " + + " path VARCHAR(255), " //NON-NLS + + " name VARCHAR(255), " //NON-NLS + + " created_time integer, " //NON-NLS + + " modified_time integer, " //NON-NLS + + " make VARCHAR(255), " //NON-NLS + + " model VARCHAR(255), " //NON-NLS + + " analyzed integer DEFAULT 0)"; //NON-NLS + stmt.execute(sql); + } catch (SQLException ex) { + logger.log(Level.SEVERE, "problem creating drawable_files table", ex); //NON-NLS + return false; + } + + String autogenKeyType = (DbType.POSTGRESQL == tskCase.getDatabaseType()) ? "BIGSERIAL" : "INTEGER"; + + // The image_gallery_groups table is created in the Case Database + try { + String tableSchema + = "( group_id " + autogenKeyType + " PRIMARY KEY, " //NON-NLS + + " data_source_obj_id integer DEFAULT 0, " + + " value VARCHAR(255) not null, " //NON-NLS + + " attribute VARCHAR(255) not null, " //NON-NLS + + " UNIQUE(data_source_obj_id, value, attribute) )"; //NON-NLS + + tskCase.getCaseDbAccessManager().createTable(GROUPS_TABLENAME, tableSchema); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "problem creating groups table", ex); //NON-NLS + return false; + } + + // The image_gallery_groups_seen table is created in the Case Database + try { + + String tableSchema + = "( id " + autogenKeyType + " PRIMARY KEY, " //NON-NLS + + " group_id integer not null, " //NON-NLS + + " examiner_id integer not null, " //NON-NLS + + " seen integer DEFAULT 0, " //NON-NLS + + " UNIQUE(group_id, examiner_id)," + + " FOREIGN KEY(group_id) REFERENCES " + GROUPS_TABLENAME + "(group_id)," + + " FOREIGN KEY(examiner_id) REFERENCES tsk_examiners(examiner_id)" + + " )"; //NON-NLS + + tskCase.getCaseDbAccessManager().createTable(GROUPS_SEEN_TABLENAME, tableSchema); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "problem creating image_gallery_groups_seen table", ex); //NON-NLS return false; } try (Statement stmt = con.createStatement()) { String sql = "CREATE TABLE if not exists hash_sets " //NON-NLS - + "( hash_set_id INTEGER primary key," //NON-NLS - + " hash_set_name VARCHAR(255) UNIQUE NOT NULL)"; //NON-NLS + + "( hash_set_id INTEGER primary key," //NON-NLS + + " hash_set_name VARCHAR(255) UNIQUE NOT NULL)"; //NON-NLS stmt.execute(sql); } catch (SQLException ex) { - LOGGER.log(Level.SEVERE, "problem creating hash_sets table", ex); //NON-NLS + logger.log(Level.SEVERE, "problem creating hash_sets table", ex); //NON-NLS return false; } try (Statement stmt = con.createStatement()) { String sql = "CREATE TABLE if not exists hash_set_hits " //NON-NLS - + "(hash_set_id INTEGER REFERENCES hash_sets(hash_set_id) not null, " //NON-NLS - + " obj_id INTEGER REFERENCES drawable_files(obj_id) not null, " //NON-NLS - + " PRIMARY KEY (hash_set_id, obj_id))"; //NON-NLS + + "(hash_set_id INTEGER REFERENCES hash_sets(hash_set_id) not null, " //NON-NLS + + " obj_id INTEGER REFERENCES drawable_files(obj_id) not null, " //NON-NLS + + " PRIMARY KEY (hash_set_id, obj_id))"; //NON-NLS stmt.execute(sql); } catch (SQLException ex) { - LOGGER.log(Level.SEVERE, "problem creating hash_set_hits table", ex); //NON-NLS + logger.log(Level.SEVERE, "problem creating hash_set_hits table", ex); //NON-NLS return false; } @@ -408,35 +510,35 @@ public final class DrawableDB { String sql = "CREATE INDEX if not exists path_idx ON drawable_files(path)"; //NON-NLS stmt.execute(sql); } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "problem creating path_idx", ex); //NON-NLS + logger.log(Level.WARNING, "problem creating path_idx", ex); //NON-NLS } try (Statement stmt = con.createStatement()) { String sql = "CREATE INDEX if not exists name_idx ON drawable_files(name)"; //NON-NLS stmt.execute(sql); } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "problem creating name_idx", ex); //NON-NLS + logger.log(Level.WARNING, "problem creating name_idx", ex); //NON-NLS } try (Statement stmt = con.createStatement()) { String sql = "CREATE INDEX if not exists make_idx ON drawable_files(make)"; //NON-NLS stmt.execute(sql); } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "problem creating make_idx", ex); //NON-NLS + logger.log(Level.WARNING, "problem creating make_idx", ex); //NON-NLS } try (Statement stmt = con.createStatement()) { String sql = "CREATE INDEX if not exists model_idx ON drawable_files(model)"; //NON-NLS stmt.execute(sql); } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "problem creating model_idx", ex); //NON-NLS + logger.log(Level.WARNING, "problem creating model_idx", ex); //NON-NLS } try (Statement stmt = con.createStatement()) { String sql = "CREATE INDEX if not exists analyzed_idx ON drawable_files(analyzed)"; //NON-NLS stmt.execute(sql); } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "problem creating analyzed_idx", ex); //NON-NLS + logger.log(Level.WARNING, "problem creating analyzed_idx", ex); //NON-NLS } return true; @@ -457,7 +559,7 @@ public final class DrawableDB { closeStatements(); con.close(); } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "Failed to close connection to drawable.db", ex); //NON-NLS + logger.log(Level.WARNING, "Failed to close connection to drawable.db", ex); //NON-NLS } } con = null; @@ -469,7 +571,7 @@ public final class DrawableDB { con = DriverManager.getConnection("jdbc:sqlite:" + dbPath.toString()); //NON-NLS } } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "Failed to open connection to drawable.db", ex); //NON-NLS + logger.log(Level.WARNING, "Failed to open connection to drawable.db", ex); //NON-NLS } } @@ -520,47 +622,107 @@ public final class DrawableDB { names.add(rs.getString(HASH_SET_NAME)); } } catch (SQLException sQLException) { - LOGGER.log(Level.WARNING, "failed to get hash set names", sQLException); //NON-NLS + logger.log(Level.WARNING, "failed to get hash set names", sQLException); //NON-NLS } finally { dbReadUnlock(); } return names; } + static private String getGroupIdQuery(GroupKey groupKey) { + // query to find the group id from attribute/value + return String.format(" SELECT group_id FROM " + GROUPS_TABLENAME + + " WHERE attribute = \'%s\' AND value = \'%s\' AND data_source_obj_id = %d", + groupKey.getAttribute().attrName.toString(), + groupKey.getValueDisplayName(), + (groupKey.getAttribute() == DrawableAttribute.PATH) ? groupKey.getDataSourceObjId() : 0); + } + + /** + * Returns true if the specified group has been any examiner + * + * @param groupKey + * + * @return + */ public boolean isGroupSeen(GroupKey groupKey) { - dbReadLock(); - try { - groupSeenQueryStmt.clearParameters(); - groupSeenQueryStmt.setString(1, groupKey.getValueDisplayName()); - groupSeenQueryStmt.setString(2, groupKey.getAttribute().attrName.toString()); - try (ResultSet rs = groupSeenQueryStmt.executeQuery()) { - while (rs.next()) { - return rs.getBoolean("seen"); //NON-NLS + return isGroupSeenByExaminer(groupKey, -1); + } + + /** + * Returns true if the specified group has been seen by the specified + * examiner + * + * @param groupKey - key to identify the group + * @param examinerId + * + * @return true if the examine has this group, false otherwise + */ + public boolean isGroupSeenByExaminer(GroupKey groupKey, long examinerId) { + + // Callback to process result of seen query + class GroupSeenQueryResultProcessor extends CompletableFuture implements CaseDbAccessQueryCallback { + + @Override + public void process(ResultSet resultSet) { + try { + if (resultSet != null) { + while (resultSet.next()) { + complete(resultSet.getInt("count") > 0); //NON-NLS; + return; + } + } + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Failed to get group seen", ex); //NON-NLS } } - } catch (SQLException ex) { - String msg = String.format("Failed to get is group seen for group key %s", groupKey.getValueDisplayName()); //NON-NLS - LOGGER.log(Level.WARNING, msg, ex); - } finally { - dbReadUnlock(); } + // Callback to process result of seen query + GroupSeenQueryResultProcessor queryResultProcessor = new GroupSeenQueryResultProcessor(); + + try { + String groupSeenQueryStmt = "COUNT(*) as count FROM " + GROUPS_SEEN_TABLENAME + + " WHERE seen = 1 " + + " AND group_id in ( " + getGroupIdQuery(groupKey) + ")" + + (examinerId > 0 ? " AND examiner_id = " + examinerId : "");// query to find the group id from attribute/value + + tskCase.getCaseDbAccessManager().select(groupSeenQueryStmt, queryResultProcessor); + return queryResultProcessor.get(); + } catch (ExecutionException | InterruptedException | TskCoreException ex) { + String msg = String.format("Failed to get is group seen for group key %s", groupKey.getValueDisplayName()); //NON-NLS + logger.log(Level.WARNING, msg, ex); + } + return false; } - public void markGroupSeen(GroupKey gk, boolean seen) { - dbWriteLock(); - try { - //PreparedStatement updateGroup = con.prepareStatement("update groups set seen = ? where value = ? and attribute = ?"); - updateGroupStmt.clearParameters(); - updateGroupStmt.setBoolean(1, seen); - updateGroupStmt.setString(2, gk.getValueDisplayName()); - updateGroupStmt.setString(3, gk.getAttribute().attrName.toString()); - updateGroupStmt.execute(); - } catch (SQLException ex) { - LOGGER.log(Level.SEVERE, "Error marking group as seen", ex); //NON-NLS - } finally { - dbWriteUnlock(); + /** + * Record in the DB that the group with the given key has the given seen + * state for the given examiner id. + * + * @param groupKey + * @param seen + * @param examinerID + * + * @throws TskCoreException + */ + public void markGroupSeen(GroupKey groupKey, boolean seen, long examinerID) throws TskCoreException { + + // query to find the group id from attribute/value + String innerQuery = String.format("( SELECT group_id FROM " + GROUPS_TABLENAME + + " WHERE attribute = \'%s\' AND value = \'%s\' and data_source_obj_id = %d )", + groupKey.getAttribute().attrName.toString(), + groupKey.getValueDisplayName(), + groupKey.getAttribute() == DrawableAttribute.PATH ? groupKey.getDataSourceObjId() : 0); + + String insertSQL = String.format(" (group_id, examiner_id, seen) VALUES (%s, %d, %d)", innerQuery, examinerID, seen ? 1 : 0); + + if (DbType.POSTGRESQL == tskCase.getDatabaseType()) { + insertSQL += String.format(" ON CONFLICT (group_id, examiner_id) DO UPDATE SET seen = %d", seen ? 1 : 0); } + + tskCase.getCaseDbAccessManager().insertOrUpdate(GROUPS_SEEN_TABLENAME, insertSQL); + } public boolean removeFile(long id) { @@ -571,23 +733,38 @@ public final class DrawableDB { } public void updateFile(DrawableFile f) { - DrawableTransaction trans = beginTransaction(); - updateFile(f, trans); - commitTransaction(trans, true); + DrawableTransaction trans = null; + CaseDbTransaction caseDbTransaction = null; + + try { + trans = beginTransaction(); + caseDbTransaction = tskCase.beginTransaction(); + updateFile(f, trans, caseDbTransaction); + caseDbTransaction.commit(); + commitTransaction(trans, true); + + } catch (TskCoreException ex) { + if (null != caseDbTransaction) { + try { + caseDbTransaction.rollback(); + } catch (TskCoreException ex2) { + logger.log(Level.SEVERE, "Error in trying to rollback transaction", ex2); //NON-NLS + } + } + if (null != trans) { + rollbackTransaction(trans); + } + logger.log(Level.SEVERE, "Error updating file", ex); //NON-NLS + } + } - public void insertFile(DrawableFile f) { - DrawableTransaction trans = beginTransaction(); - insertFile(f, trans); - commitTransaction(trans, true); + public void insertFile(DrawableFile f, DrawableTransaction tr, CaseDbTransaction caseDbTransaction) { + insertOrUpdateFile(f, tr, insertFileStmt, caseDbTransaction); } - public void insertFile(DrawableFile f, DrawableTransaction tr) { - insertOrUpdateFile(f, tr, insertFileStmt); - } - - public void updateFile(DrawableFile f, DrawableTransaction tr) { - insertOrUpdateFile(f, tr, updateFileStmt); + public void updateFile(DrawableFile f, DrawableTransaction tr, CaseDbTransaction caseDbTransaction) { + insertOrUpdateFile(f, tr, updateFileStmt, caseDbTransaction); } /** @@ -603,7 +780,7 @@ public final class DrawableDB { * @param tr a transaction to use, must not be null * @param stmt the statement that does the actull inserting */ - private void insertOrUpdateFile(DrawableFile f, @Nonnull DrawableTransaction tr, @Nonnull PreparedStatement stmt) { + private void insertOrUpdateFile(DrawableFile f, @Nonnull DrawableTransaction tr, @Nonnull PreparedStatement stmt, @Nonnull CaseDbTransaction caseDbTransaction) { if (tr.isClosed()) { throw new IllegalArgumentException("can't update database with closed transaction"); @@ -611,15 +788,16 @@ public final class DrawableDB { dbWriteLock(); try { - // "INSERT OR IGNORE/ INTO drawable_files (path, name, created_time, modified_time, make, model, analyzed)" + // "INSERT OR IGNORE/ INTO drawable_files (obj_id, data_source_obj_id, path, name, created_time, modified_time, make, model, analyzed)" stmt.setLong(1, f.getId()); - stmt.setString(2, f.getDrawablePath()); - stmt.setString(3, f.getName()); - stmt.setLong(4, f.getCrtime()); - stmt.setLong(5, f.getMtime()); - stmt.setString(6, f.getMake()); - stmt.setString(7, f.getModel()); - stmt.setBoolean(8, f.isAnalyzed()); + stmt.setLong(2, f.getAbstractFile().getDataSource().getId()); + stmt.setString(3, f.getDrawablePath()); + stmt.setString(4, f.getName()); + stmt.setLong(5, f.getCrtime()); + stmt.setLong(6, f.getMtime()); + stmt.setString(7, f.getMake()); + stmt.setString(8, f.getModel()); + stmt.setBoolean(9, f.isAnalyzed()); stmt.executeUpdate(); // Update the list of file IDs in memory addImageFileToList(f.getId()); @@ -646,29 +824,32 @@ public final class DrawableDB { } } } catch (TskCoreException ex) { - LOGGER.log(Level.SEVERE, "failed to insert/update hash hits for file" + f.getContentPathSafe(), ex); //NON-NLS + logger.log(Level.SEVERE, "failed to insert/update hash hits for file" + f.getContentPathSafe(), ex); //NON-NLS } //and update all groups this file is in for (DrawableAttribute attr : DrawableAttribute.getGroupableAttrs()) { Collection> vals = attr.getValue(f); for (Comparable val : vals) { - //use empty string for null values (mime_type), this shouldn't happen! if (null != val) { - insertGroup(val.toString(), attr); + if (attr == DrawableAttribute.PATH) { + insertGroup(f.getAbstractFile().getDataSource().getId(), val.toString(), attr, caseDbTransaction); + } else { + insertGroup(val.toString(), attr, caseDbTransaction); + } } } } tr.addUpdatedFile(f.getId()); - } catch (SQLException | NullPointerException ex) { + } catch (SQLException | NullPointerException | TskCoreException ex) { /* * This is one of the places where we get an error if the case is * closed during processing, which doesn't need to be reported here. */ if (Case.isCaseOpen()) { - LOGGER.log(Level.SEVERE, "failed to insert/update file" + f.getContentPathSafe(), ex); //NON-NLS + logger.log(Level.SEVERE, "failed to insert/update file" + f.getContentPathSafe(), ex); //NON-NLS } } finally { @@ -676,6 +857,71 @@ public final class DrawableDB { } } + /** + * Gets all data source object ids from datasources table, and their + * DrawableDbBuildStatusEnum + * + * @return map of known data source object ids, and their db status + * + * @throws org.sleuthkit.datamodel.TskCoreException + */ + public Map getDataSourceDbBuildStatus() throws TskCoreException { + Statement statement = null; + ResultSet rs = null; + Map map = new HashMap<>(); + dbReadLock(); + try { + statement = con.createStatement(); + rs = statement.executeQuery("SELECT ds_obj_id, drawable_db_build_status FROM datasources "); //NON-NLS + while (rs.next()) { + map.put(rs.getLong("ds_obj_id"), DrawableDbBuildStatusEnum.valueOf(rs.getString("drawable_db_build_status"))); + } + } catch (SQLException e) { + throw new TskCoreException("SQLException while getting data source object ids", e); + } finally { + if (rs != null) { + try { + rs.close(); + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Error closing resultset", ex); //NON-NLS + } + } + if (statement != null) { + try { + statement.close(); + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Error closing statement ", ex); //NON-NLS + } + } + dbReadUnlock(); + } + return map; + } + + /** + * Insert/update given data source object id and it's DB rebuild status in + * the datasources table. + * + * If the object id exists in the table already, it updates the status + * + * @param dsObjectId data source object id to insert + * @param status The db build statsus for datasource. + */ + public void insertOrUpdateDataSource(long dsObjectId, DrawableDbBuildStatusEnum status) { + dbWriteLock(); + try { + // "INSERT OR REPLACE INTO datasources (ds_obj_id, drawable_db_build_status) " //NON-NLS + updateDataSourceStmt.setLong(1, dsObjectId); + updateDataSourceStmt.setString(2, status.name()); + + updateDataSourceStmt.executeUpdate(); + } catch (SQLException | NullPointerException ex) { + logger.log(Level.SEVERE, "failed to insert/update datasources table", ex); //NON-NLS + } finally { + dbWriteUnlock(); + } + } + public DrawableTransaction beginTransaction() { return new DrawableTransaction(); } @@ -687,6 +933,13 @@ public final class DrawableDB { tr.commit(notify); } + public void rollbackTransaction(DrawableTransaction tr) { + if (tr.isClosed()) { + throw new IllegalArgumentException("can't rollback already closed transaction"); + } + tr.rollback(); + } + public Boolean isFileAnalyzed(DrawableFile f) { return isFileAnalyzed(f.getId()); } @@ -700,7 +953,7 @@ public final class DrawableDB { } } catch (SQLException ex) { String msg = String.format("Failed to determine if file %s is finalized", String.valueOf(fileId)); //NON-NLS - LOGGER.log(Level.WARNING, msg, ex); + logger.log(Level.WARNING, msg, ex); } finally { dbReadUnlock(); } @@ -718,7 +971,7 @@ public final class DrawableDB { return analyzedQuery.getInt(ANALYZED) == fileIds.size(); } } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "problem counting analyzed files: ", ex); //NON-NLS + logger.log(Level.WARNING, "problem counting analyzed files: ", ex); //NON-NLS } finally { dbReadUnlock(); } @@ -730,7 +983,6 @@ public final class DrawableDB { dbReadLock(); try { Set fileIDsInGroup = getFileIDsInGroup(gk); - try { // In testing, this method appears to be a lot faster than doing one large select statement for (Long fileID : fileIDsInGroup) { @@ -745,10 +997,10 @@ public final class DrawableDB { } } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "problem counting analyzed files: ", ex); //NON-NLS + logger.log(Level.WARNING, "problem counting analyzed files: ", ex); //NON-NLS } } catch (TskCoreException tskCoreException) { - LOGGER.log(Level.WARNING, "problem counting analyzed files: ", tskCoreException); //NON-NLS + logger.log(Level.WARNING, "problem counting analyzed files: ", tskCoreException); //NON-NLS } finally { dbReadUnlock(); } @@ -768,33 +1020,18 @@ public final class DrawableDB { * @throws TskCoreException */ public Set findAllFileIdsWhere(String sqlWhereClause) throws TskCoreException { - Statement statement = null; - ResultSet rs = null; + Set ret = new HashSet<>(); dbReadLock(); - try { - statement = con.createStatement(); - rs = statement.executeQuery("SELECT obj_id FROM drawable_files WHERE " + sqlWhereClause); //NON-NLS + try (Statement statement = con.createStatement(); + ResultSet rs = statement.executeQuery("SELECT obj_id FROM drawable_files WHERE " + sqlWhereClause);) { while (rs.next()) { ret.add(rs.getLong(1)); } } catch (SQLException e) { throw new TskCoreException("SQLException thrown when calling 'DrawableDB.findAllFileIdsWhere(): " + sqlWhereClause, e); } finally { - if (rs != null) { - try { - rs.close(); - } catch (SQLException ex) { - LOGGER.log(Level.SEVERE, "Error closing result set after executing findAllFileIdsWhere", ex); //NON-NLS - } - } - if (statement != null) { - try { - statement.close(); - } catch (SQLException ex) { - LOGGER.log(Level.SEVERE, "Error closing statement after executing findAllFileIdsWhere", ex); //NON-NLS - } - } + dbReadUnlock(); } return ret; @@ -812,47 +1049,36 @@ public final class DrawableDB { * @throws TskCoreException */ public long countFilesWhere(String sqlWhereClause) throws TskCoreException { - Statement statement = null; - ResultSet rs = null; dbReadLock(); - try { - statement = con.createStatement(); - rs = statement.executeQuery("SELECT COUNT (*) FROM drawable_files WHERE " + sqlWhereClause); //NON-NLS - return rs.getLong(1); + try (Statement statement = con.createStatement(); + ResultSet rs = statement.executeQuery("SELECT COUNT(*) AS COUNT FROM drawable_files WHERE " + sqlWhereClause);) { + return rs.getLong("COUNT"); } catch (SQLException e) { throw new TskCoreException("SQLException thrown when calling 'DrawableDB.countFilesWhere(): " + sqlWhereClause, e); } finally { - if (rs != null) { - try { - rs.close(); - } catch (SQLException ex) { - LOGGER.log(Level.SEVERE, "Error closing result set after executing countFilesWhere", ex); //NON-NLS - } - } - if (statement != null) { - try { - statement.close(); - } catch (SQLException ex) { - LOGGER.log(Level.SEVERE, "Error closing statement after executing countFilesWhere", ex); //NON-NLS - } - } dbReadUnlock(); } } /** + * Get all the values that are in db for the given attribute. * * - * - * @param groupBy - * @param sortBy - * @param sortOrder + * @param The type of values for the given attribute. + * @param groupBy The attribute to get the values for. + * @param sortBy The way to sort the results. Only GROUP_BY_VAL and + * FILE_COUNT are supported. + * @param sortOrder Sort ascending or descending. + * @param dataSource * * @return + * + * @throws org.sleuthkit.datamodel.TskCoreException */ - public > List findValuesForAttribute(DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder) { + @SuppressWarnings("unchecked") + public > Multimap findValuesForAttribute(DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder, DataSource dataSource) throws TskCoreException { - List vals = new ArrayList<>(); + Multimap values = HashMultimap.create(); switch (groupBy.attrName) { case ANALYZED: @@ -864,7 +1090,14 @@ public final class DrawableDB { default: dbReadLock(); //TODO: convert this to prepared statement - StringBuilder query = new StringBuilder("SELECT " + groupBy.attrName.toString() + ", COUNT(*) FROM drawable_files GROUP BY " + groupBy.attrName.toString()); //NON-NLS + + StringBuilder query = new StringBuilder("SELECT data_source_obj_id, " + groupBy.attrName.toString() + ", COUNT(*) FROM drawable_files "); //NON-NLS + + if (dataSource != null) { + query.append(" WHERE data_source_obj_id = ").append(dataSource.getId()); + } + + query.append(" GROUP BY data_source_obj_id, ").append(groupBy.attrName.toString()); String orderByClause = ""; @@ -893,49 +1126,73 @@ public final class DrawableDB { } try (Statement stmt = con.createStatement(); - ResultSet valsResults = stmt.executeQuery(query.toString())) { - while (valsResults.next()) { + ResultSet results = stmt.executeQuery(query.toString())) { + while (results.next()) { /* - * I don't like that we have to do this cast here, but - * can't think of a better alternative at the momment - * unless something has gone seriously wrong, we know - * this should be of type A even if JAVA doesn't + * I don't like that we have to do this cast to A here, + * but can't think of a better alternative at the + * momment unless something has gone seriously wrong, we + * know this should be of type A even if JAVA doesn't */ - @SuppressWarnings("unchecked") - A value = (A) valsResults.getObject(groupBy.attrName.toString()); - vals.add(value); + values.put(tskCase.getDataSource(results.getLong("data_source_obj_id")), + (A) results.getObject(groupBy.attrName.toString())); } } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "Unable to get values for attribute", ex); //NON-NLS + if (!(ex.getCause() instanceof java.lang.InterruptedException)) { + + /* It seems like this originaly comes out of c3p0 when + * its thread is intereupted (cancelled because of + * regroup). It should be safe to just swallow this and + * move on. + * + * see + * https://sourceforge.net/p/c3p0/mailman/c3p0-users/thread/EBB32BB8-6487-43AF-B291-9464C9051869@mchange.com/ + */ + throw new TskCoreException("Unable to get values for attribute", ex); //NON-NLS + } + } catch (TskDataException ex) { + throw new TskCoreException("Unable to get values for attribute", ex); //NON-NLS } finally { dbReadUnlock(); } } - return vals; + return values; } /** * Insert new group into DB - * @param value Value of the group (unique to the type) - * @param groupBy Type of the grouping (CATEGORY, MAKE, etc.) + * + * @param value Value of the group (unique to the type) + * @param groupBy Type of the grouping (CATEGORY, MAKE, etc.) + * @param caseDbTransaction transaction to use for CaseDB insert/updates */ - private void insertGroup(final String value, DrawableAttribute groupBy) { - dbWriteLock(); + private void insertGroup(final String value, DrawableAttribute groupBy, CaseDbTransaction caseDbTransaction) { + insertGroup(0, value, groupBy, caseDbTransaction); + } + /** + * Insert new group into DB + * + * @param ds_obj_id data source object id + * @param value Value of the group (unique to the type) + * @param groupBy Type of the grouping (CATEGORY, MAKE, etc.) + * @param caseDbTransaction transaction to use for CaseDB insert/updates + */ + private void insertGroup(long ds_obj_id, final String value, DrawableAttribute groupBy, CaseDbTransaction caseDbTransaction) { try { - //PreparedStatement insertGroup = con.prepareStatement("insert or replace into groups (value, attribute, seen) values (?,?,0)"); - insertGroupStmt.clearParameters(); - insertGroupStmt.setString(1, value); - insertGroupStmt.setString(2, groupBy.attrName.toString()); - insertGroupStmt.execute(); - } catch (SQLException sQLException) { + String insertSQL = String.format(" (data_source_obj_id, value, attribute) VALUES (%d, \'%s\', \'%s\')", + ds_obj_id, value, groupBy.attrName.toString()); + + if (DbType.POSTGRESQL == tskCase.getDatabaseType()) { + insertSQL += "ON CONFLICT DO NOTHING"; + } + tskCase.getCaseDbAccessManager().insert(GROUPS_TABLENAME, insertSQL, caseDbTransaction); + } catch (TskCoreException ex) { // Don't need to report it if the case was closed if (Case.isCaseOpen()) { - LOGGER.log(Level.SEVERE, "Unable to insert group", sQLException); //NON-NLS + logger.log(Level.SEVERE, "Unable to insert group", ex); //NON-NLS } - } finally { - dbWriteUnlock(); } } @@ -953,8 +1210,8 @@ public final class DrawableDB { return DrawableFile.create(f, areFilesAnalyzed(Collections.singleton(id)), isVideoFile(f)); } catch (IllegalStateException ex) { - LOGGER.log(Level.SEVERE, "there is no case open; failed to load file with id: {0}", id); //NON-NLS - return null; + logger.log(Level.SEVERE, "there is no case open; failed to load file with id: {0}", id); //NON-NLS + throw new TskCoreException("there is no case open; failed to load file with id: " + id, ex); } } @@ -973,8 +1230,8 @@ public final class DrawableDB { Set files = new HashSet<>(); dbReadLock(); try { - PreparedStatement statement = getGroupStatment(groupKey.getAttribute()); - statement.setObject(1, groupKey.getValue()); + PreparedStatement statement = getGroupStatment(groupKey); + setQueryParams(statement, groupKey); try (ResultSet valsResults = statement.executeQuery()) { while (valsResults.next()) { @@ -982,7 +1239,7 @@ public final class DrawableDB { } } } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "failed to get file for group:" + groupKey.getAttribute() + " == " + groupKey.getValue(), ex); //NON-NLS + logger.log(Level.WARNING, "failed to get file for group:" + groupKey.getAttribute() + " == " + groupKey.getValue(), ex); //NON-NLS } finally { dbReadUnlock(); } @@ -996,26 +1253,26 @@ public final class DrawableDB { } } - private PreparedStatement getGroupStatment(DrawableAttribute groupBy) { - return groupStatementMap.get(groupBy); + private PreparedStatement getGroupStatment(GroupKey groupKey) { + DrawableAttribute groupBy = groupKey.getAttribute(); + if ((groupBy == DrawableAttribute.PATH) && groupKey.getDataSource().isPresent()) { + return this.groupStatementFilterByDataSrcMap.get(groupBy); + } + + return groupStatementMap.get(groupBy); } - public int countAllFiles() { - int result = -1; - dbReadLock(); - try (ResultSet rs = con.createStatement().executeQuery("SELECT COUNT(*) AS COUNT FROM drawable_files")) { //NON-NLS - while (rs.next()) { + public long countAllFiles() throws TskCoreException { + return countAllFiles(null); + } - result = rs.getInt("COUNT"); - break; - } - } catch (SQLException ex) { - LOGGER.log(Level.SEVERE, "Error accessing SQLite database"); //NON-NLS - } finally { - dbReadUnlock(); + public long countAllFiles(DataSource dataSource) throws TskCoreException { + if (null != dataSource) { + return countFilesWhere(" data_source_obj_id = "); + } else { + return countFilesWhere(" 1 "); } - return result; } /** @@ -1043,7 +1300,7 @@ public final class DrawableDB { //TODO: delete from hash_set_hits table also... } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "failed to delete row for obj_id = " + id, ex); //NON-NLS + logger.log(Level.WARNING, "failed to delete row for obj_id = " + id, ex); //NON-NLS } finally { dbWriteUnlock(); } @@ -1055,10 +1312,8 @@ public final class DrawableDB { public class MultipleTransactionException extends IllegalStateException { - private static final String CANNOT_HAVE_MORE_THAN_ONE_OPEN_TRANSACTIO = "cannot have more than one open transaction"; //NON-NLS - public MultipleTransactionException() { - super(CANNOT_HAVE_MORE_THAN_ONE_OPEN_TRANSACTIO); + super("cannot have more than one open transaction");//NON-NLS } } @@ -1097,14 +1352,13 @@ public final class DrawableDB { private void initializeImageList() { synchronized (fileIDsInDB) { dbReadLock(); - try { - Statement stmt = con.createStatement(); - ResultSet analyzedQuery = stmt.executeQuery("select obj_id from drawable_files"); //NON-NLS + try (Statement stmt = con.createStatement(); + ResultSet analyzedQuery = stmt.executeQuery("select obj_id from drawable_files");) { while (analyzedQuery.next()) { addImageFileToList(analyzedQuery.getLong(OBJ_ID)); } } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "problem loading file IDs: ", ex); //NON-NLS + logger.log(Level.WARNING, "problem loading file IDs: ", ex); //NON-NLS } finally { dbReadUnlock(); } @@ -1155,9 +1409,9 @@ public final class DrawableDB { .count(); } } catch (IllegalStateException ex) { - LOGGER.log(Level.WARNING, "Case closed while getting files"); //NON-NLS + logger.log(Level.WARNING, "Case closed while getting files"); //NON-NLS } catch (TskCoreException ex1) { - LOGGER.log(Level.SEVERE, "Failed to get content tags by tag name.", ex1); //NON-NLS + logger.log(Level.SEVERE, "Failed to get content tags by tag name.", ex1); //NON-NLS } return -1; @@ -1176,20 +1430,20 @@ public final class DrawableDB { * * @param fileIDs the the files ids to count within * - * @return the number of files with Cat-0 + * @return the number of files in the given set with Cat-0 */ - public long getUncategorizedCount(Collection fileIDs) { - + public long getUncategorizedCount(Collection fileIDs) throws TskCoreException { + // if the fileset is empty, return count as 0 if (fileIDs.isEmpty()) { return 0; } - - DrawableTagsManager tagsManager = controller.getTagsManager(); // get a comma seperated list of TagName ids for non zero categories - String catTagNameIDs = DhsImageCategory.getNonZeroCategories().stream() - .map(tagsManager::getTagName) + DrawableTagsManager tagsManager = controller.getTagsManager(); + + String catTagNameIDs = tagsManager.getCategoryTagNames().stream() + .filter(tagName -> notEqual(tagName.getDisplayName(), DhsImageCategory.ZERO.getDisplayName())) .map(TagName::getId) .map(Object::toString) .collect(Collectors.joining(",", "(", ")")); @@ -1197,17 +1451,18 @@ public final class DrawableDB { String fileIdsList = "(" + StringUtils.join(fileIDs, ",") + " )"; //count the file ids that are in the given list and don't have a non-zero category assigned to them. - String name = - "SELECT COUNT(obj_id) as obj_count FROM tsk_files where obj_id IN " + fileIdsList //NON-NLS - + " AND obj_id NOT IN (SELECT obj_id FROM content_tags WHERE content_tags.tag_name_id IN " + catTagNameIDs + ")"; //NON-NLS + String name + = "SELECT COUNT(obj_id) as obj_count FROM tsk_files where obj_id IN " + fileIdsList //NON-NLS + + " AND obj_id NOT IN (SELECT obj_id FROM content_tags WHERE content_tags.tag_name_id IN " + catTagNameIDs + ")"; //NON-NLS try (SleuthkitCase.CaseDbQuery executeQuery = tskCase.executeQuery(name); ResultSet resultSet = executeQuery.getResultSet();) { while (resultSet.next()) { return resultSet.getLong("obj_count"); //NON-NLS } - } catch (SQLException | TskCoreException ex) { - LOGGER.log(Level.SEVERE, "Error getting category count.", ex); //NON-NLS + } catch (SQLException ex) { + throw new TskCoreException("Error getting category count.", ex); //NON-NLS } + return -1; } @@ -1240,7 +1495,7 @@ public final class DrawableDB { con.setAutoCommit(false); } catch (SQLException ex) { - LOGGER.log(Level.SEVERE, "failed to set auto-commit to to false", ex); //NON-NLS + logger.log(Level.SEVERE, "failed to set auto-commit to to false", ex); //NON-NLS } } @@ -1251,7 +1506,7 @@ public final class DrawableDB { con.rollback(); updatedFiles.clear(); } catch (SQLException ex1) { - LOGGER.log(Level.SEVERE, "Exception while attempting to rollback!!", ex1); //NON-NLS + logger.log(Level.SEVERE, "Exception while attempting to rollback!!", ex1); //NON-NLS } finally { close(); } @@ -1273,9 +1528,9 @@ public final class DrawableDB { } } catch (SQLException ex) { if (Case.isCaseOpen()) { - LOGGER.log(Level.SEVERE, "Error commiting drawable.db.", ex); //NON-NLS + logger.log(Level.SEVERE, "Error commiting drawable.db.", ex); //NON-NLS } else { - LOGGER.log(Level.WARNING, "Error commiting drawable.db - case is closed."); //NON-NLS + logger.log(Level.WARNING, "Error commiting drawable.db - case is closed."); //NON-NLS } rollback(); } @@ -1288,9 +1543,9 @@ public final class DrawableDB { con.setAutoCommit(true); } catch (SQLException ex) { if (Case.isCaseOpen()) { - LOGGER.log(Level.SEVERE, "Error setting auto-commit to true.", ex); //NON-NLS + logger.log(Level.SEVERE, "Error setting auto-commit to true.", ex); //NON-NLS } else { - LOGGER.log(Level.SEVERE, "Error setting auto-commit to true - case is closed"); //NON-NLS + logger.log(Level.SEVERE, "Error setting auto-commit to true - case is closed"); //NON-NLS } } finally { closed = true; diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java index 34c9d5a4c5..e22151f934 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.imagegallery.datamodel; -import org.sleuthkit.autopsy.datamodel.DhsImageCategory; import java.lang.ref.SoftReference; import java.text.MessageFormat; import java.util.ArrayList; @@ -32,6 +31,7 @@ import java.util.stream.Collectors; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.concurrent.Task; +import javafx.concurrent.Worker; import javafx.scene.image.Image; import javafx.util.Pair; import javax.annotation.Nonnull; @@ -40,8 +40,8 @@ import org.apache.commons.lang3.text.WordUtils; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datamodel.DhsImageCategory; import org.sleuthkit.autopsy.imagegallery.FileTypeUtils; -import org.sleuthkit.autopsy.imagegallery.ThumbnailCache; import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -67,15 +67,21 @@ public abstract class DrawableFile { /** * Skip the database query if we have already determined the file type. + * + * @param file The underlying AbstractFile. + * @param analyzed Is the file analyzed. + * @param isVideo Is the file a video. + * + * @return */ - public static DrawableFile create(AbstractFile abstractFileById, boolean analyzed, boolean isVideo) { + public static DrawableFile create(AbstractFile file, boolean analyzed, boolean isVideo) { return isVideo - ? new VideoFile(abstractFileById, analyzed) - : new ImageFile(abstractFileById, analyzed); + ? new VideoFile(file, analyzed) + : new ImageFile(file, analyzed); } - public static DrawableFile create(Long id, boolean analyzed) throws TskCoreException, NoCurrentCaseException { - return create(Case.getCurrentCaseThrows().getSleuthkitCase().getAbstractFileById(id), analyzed); + public static DrawableFile create(Long fileID, boolean analyzed) throws TskCoreException, NoCurrentCaseException { + return create(Case.getCurrentCaseThrows().getSleuthkitCase().getAbstractFileById(fileID), analyzed); } private SoftReference imageRef; @@ -149,8 +155,8 @@ public abstract class DrawableFile { return file.getSleuthkitCase(); } - private Pair, Collection> makeAttributeValuePair(DrawableAttribute t) { - return new Pair<>(t, t.getValue(DrawableFile.this)); + private Pair, Collection> makeAttributeValuePair(DrawableAttribute attribute) { + return new Pair<>(attribute, attribute.getValue(this)); } public String getModel() { @@ -254,42 +260,17 @@ public abstract class DrawableFile { return getSleuthkitCase().getContentTagsByContent(file); } - @Deprecated - public Image getThumbnail() { - try { - return getThumbnailTask().get(); - } catch (InterruptedException | ExecutionException ex) { - return null; - } - - } - - public Task getThumbnailTask() { - return ThumbnailCache.getDefault().getThumbnailTask(this); - } - - @Deprecated //use non-blocking getReadFullSizeImageTask instead for most cases - public Image getFullSizeImage() { - try { - return getReadFullSizeImageTask().get(); - } catch (InterruptedException | ExecutionException ex) { - return null; - } - } - public Task getReadFullSizeImageTask() { Image image = (imageRef != null) ? imageRef.get() : null; if (image == null || image.isError()) { Task readImageTask = getReadFullSizeImageTaskHelper(); readImageTask.stateProperty().addListener(stateProperty -> { - switch (readImageTask.getState()) { - case SUCCEEDED: - try { - imageRef = new SoftReference<>(readImageTask.get()); - } catch (InterruptedException | ExecutionException exception) { - LOGGER.log(Level.WARNING, getMessageTemplate(exception), getContentPathSafe()); - } - break; + if (readImageTask.getState() == Worker.State.SUCCEEDED) { + try { + imageRef = new SoftReference<>(readImageTask.get()); + } catch (InterruptedException | ExecutionException exception) { + LOGGER.log(Level.WARNING, getMessageTemplate(exception), getContentPathSafe()); + } } }); return readImageTask; @@ -316,14 +297,14 @@ public abstract class DrawableFile { /** * Get the width of the visual content. - * + * * @return The width. */ abstract Double getWidth(); /** * Get the height of the visual content. - * + * * @return The height. */ abstract Double getHeight(); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableTagsManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableTagsManager.java index cb30c37bab..90b7b5c398 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableTagsManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableTagsManager.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-16 Basis Technology Corp. + * Copyright 2013-18 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,25 +18,23 @@ */ package org.sleuthkit.autopsy.imagegallery.datamodel; -import org.sleuthkit.autopsy.datamodel.DhsImageCategory; import com.google.common.eventbus.AsyncEventBus; import com.google.common.eventbus.EventBus; -import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.stream.Collectors; import javafx.scene.Node; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javax.annotation.Nonnull; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.casemodule.services.TagsManager; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datamodel.DhsImageCategory; +import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.TagName; @@ -44,39 +42,38 @@ import org.sleuthkit.datamodel.TskCoreException; /** * Manages Tags, Tagging, and the relationship between Categories and Tags in - * the autopsy Db. Delegates some work to the backing {@link TagsManager}. + * the autopsy Db. Delegates some work to the backing autopsy TagsManager. */ @NbBundle.Messages({"DrawableTagsManager.followUp=Follow Up", "DrawableTagsManager.bookMark=Bookmark"}) -public class DrawableTagsManager { +public final class DrawableTagsManager { - private static final Logger LOGGER = Logger.getLogger(DrawableTagsManager.class.getName()); + private static final Logger logger = Logger.getLogger(DrawableTagsManager.class.getName()); - private static Image FOLLOW_UP_IMAGE; - private static Image BOOKMARK_IMAGE; + private static final Image FOLLOW_UP_IMAGE = new Image("/org/sleuthkit/autopsy/imagegallery/images/flag_red.png"); + private static final Image BOOKMARK_IMAGE = new Image("/org/sleuthkit/autopsy/images/star-bookmark-icon-16.png"); - final private Object autopsyTagsManagerLock = new Object(); - private TagsManager autopsyTagsManager; + private final TagsManager autopsyTagsManager; + /** The tag name corresponding to the "built-in" tag "Follow Up" */ + private final TagName followUpTagName; + private final TagName bookmarkTagName; /** - * Used to distribute {@link TagsChangeEvent}s + * Used to distribute TagsChangeEvents */ - private final EventBus tagsEventBus = new AsyncEventBus( - Executors.newSingleThreadExecutor( - new BasicThreadFactory.Builder().namingPattern("Tags Event Bus").uncaughtExceptionHandler((Thread t, Throwable e) -> { //NON-NLS - LOGGER.log(Level.SEVERE, "uncaught exception in event bus handler", e); //NON-NLS - }).build() - )); - - /** - * The tag name corresponding to the "built-in" tag "Follow Up" - */ - private TagName followUpTagName; - private TagName bookmarkTagName; - - public DrawableTagsManager(TagsManager autopsyTagsManager) { - this.autopsyTagsManager = autopsyTagsManager; + private final EventBus tagsEventBus + = new AsyncEventBus( + Executors.newSingleThreadExecutor( + new BasicThreadFactory.Builder() + .namingPattern("Tags Event Bus")//NON-NLS + .uncaughtExceptionHandler((Thread thread, Throwable throwable) + -> logger.log(Level.SEVERE, "Uncaught exception in DrawableTagsManager event bus handler.", throwable)) //NON-NLS + .build())); + public DrawableTagsManager(ImageGalleryController controller) throws TskCoreException { + this.autopsyTagsManager = controller.getAutopsyCase().getServices().getTagsManager(); + followUpTagName = getTagName(Bundle.DrawableTagsManager_followUp()); + bookmarkTagName = getTagName(Bundle.DrawableTagsManager_bookMark()); } /** @@ -106,74 +103,51 @@ public class DrawableTagsManager { } /** - * assign a new TagsManager to back this one, ie when the current case - * changes + * Get the follow up TagName. * - * @param autopsyTagsManager + * @return The follow up TagName. */ - public void setAutopsyTagsManager(TagsManager autopsyTagsManager) { - synchronized (autopsyTagsManagerLock) { - this.autopsyTagsManager = autopsyTagsManager; - clearFollowUpTagName(); - } + public TagName getFollowUpTagName() { + return followUpTagName; } /** - * Use when closing a case to make sure everything is re-initialized in the - * next case. + * Get the bookmark TagName. + * + * @return The bookmark TagName. */ - public void clearFollowUpTagName() { - synchronized (autopsyTagsManagerLock) { - followUpTagName = null; - } + private TagName getBookmarkTagName() throws TskCoreException { + return bookmarkTagName; } /** - * get the (cached) follow up TagName + * Get all the TagNames that are not categories * - * @return + * @return All the TagNames that are not categories, in alphabetical order + * by displayName. * - * @throws TskCoreException + * @throws org.sleuthkit.datamodel.TskCoreException */ - public TagName getFollowUpTagName() throws TskCoreException { - synchronized (autopsyTagsManagerLock) { - if (Objects.isNull(followUpTagName)) { - followUpTagName = getTagName(NbBundle.getMessage(DrawableTagsManager.class, "DrawableTagsManager.followUp")); - } - return followUpTagName; - } + public List getNonCategoryTagNames() throws TskCoreException { + return autopsyTagsManager.getAllTagNames().stream() + .filter(CategoryManager::isNotCategoryTagName) + .distinct().sorted() + .collect(Collectors.toList()); } - private Object getBookmarkTagName() throws TskCoreException { - synchronized (autopsyTagsManagerLock) { - if (Objects.isNull(bookmarkTagName)) { - bookmarkTagName = getTagName(NbBundle.getMessage(DrawableTagsManager.class, "DrawableTagsManager.bookMark")); - } - return bookmarkTagName; - } - } - - /** - * get all the TagNames that are not categories + * Get all the TagNames that are categories * - * @return all the TagNames that are not categories, in alphabetical order - * by displayName, or, an empty set if there was an exception - * looking them up from the db. + * @return All the TagNames that are categories, in alphabetical order by + * displayName. + * + * @throws org.sleuthkit.datamodel.TskCoreException */ - @Nonnull - public List getNonCategoryTagNames() { - synchronized (autopsyTagsManagerLock) { - try { - return autopsyTagsManager.getAllTagNames().stream() - .filter(CategoryManager::isNotCategoryTagName) - .distinct().sorted() - .collect(Collectors.toList()); - } catch (TskCoreException | IllegalStateException ex) { - LOGGER.log(Level.WARNING, "couldn't access case", ex); //NON-NLS - } - return Collections.emptyList(); - } + public List getCategoryTagNames() throws TskCoreException { + return autopsyTagsManager.getAllTagNames().stream() + .filter(CategoryManager::isCategoryTagName) + .distinct().sorted() + .collect(Collectors.toList()); } /** @@ -187,9 +161,7 @@ public class DrawableTagsManager { * @throws TskCoreException if there was an error reading from the db */ public List getContentTags(Content content) throws TskCoreException { - synchronized (autopsyTagsManagerLock) { - return autopsyTagsManager.getContentTagsByContent(content); - } + return autopsyTagsManager.getContentTagsByContent(content); } /** @@ -207,91 +179,56 @@ public class DrawableTagsManager { } public TagName getTagName(String displayName) throws TskCoreException { - synchronized (autopsyTagsManagerLock) { - try { - TagName returnTagName = autopsyTagsManager.getDisplayNamesToTagNamesMap().get(displayName); - if (returnTagName != null) { - return returnTagName; - } - try { - return autopsyTagsManager.addTagName(displayName); - } catch (TagsManager.TagNameAlreadyExistsException ex) { - returnTagName = autopsyTagsManager.getDisplayNamesToTagNamesMap().get(displayName); - if (returnTagName != null) { - return returnTagName; - } - throw new TskCoreException("Tag name exists but an error occured in retrieving it", ex); - } - } catch (NullPointerException | IllegalStateException ex) { - LOGGER.log(Level.SEVERE, "Case was closed out from underneath", ex); //NON-NLS - throw new TskCoreException("Case was closed out from underneath", ex); + + TagName returnTagName = autopsyTagsManager.getDisplayNamesToTagNamesMap().get(displayName); + if (returnTagName != null) { + return returnTagName; + } + try { + return autopsyTagsManager.addTagName(displayName); + } catch (TagsManager.TagNameAlreadyExistsException ex) { + returnTagName = autopsyTagsManager.getDisplayNamesToTagNamesMap().get(displayName); + if (returnTagName != null) { + return returnTagName; } + throw new TskCoreException("Tag name exists but an error occured in retrieving it", ex); } } - public TagName getTagName(DhsImageCategory cat) { - try { - return getTagName(cat.getDisplayName()); - } catch (TskCoreException ex) { - return null; - } + public TagName getTagName(DhsImageCategory cat) throws TskCoreException { + return getTagName(cat.getDisplayName()); } public ContentTag addContentTag(DrawableFile file, TagName tagName, String comment) throws TskCoreException { - synchronized (autopsyTagsManagerLock) { - return autopsyTagsManager.addContentTag(file.getAbstractFile(), tagName, comment); - } + return autopsyTagsManager.addContentTag(file.getAbstractFile(), tagName, comment); } - public List getContentTagsByTagName(TagName t) throws TskCoreException { - synchronized (autopsyTagsManagerLock) { - return autopsyTagsManager.getContentTagsByTagName(t); - } + public List getContentTagsByTagName(TagName tagName) throws TskCoreException { + return autopsyTagsManager.getContentTagsByTagName(tagName); } public List getAllTagNames() throws TskCoreException { - synchronized (autopsyTagsManagerLock) { - return autopsyTagsManager.getAllTagNames(); - } + return autopsyTagsManager.getAllTagNames(); } public List getTagNamesInUse() throws TskCoreException { - synchronized (autopsyTagsManagerLock) { - return autopsyTagsManager.getTagNamesInUse(); - } + return autopsyTagsManager.getTagNamesInUse(); } - public void deleteContentTag(ContentTag ct) throws TskCoreException { - synchronized (autopsyTagsManagerLock) { - autopsyTagsManager.deleteContentTag(ct); - } + public void deleteContentTag(ContentTag contentTag) throws TskCoreException { + autopsyTagsManager.deleteContentTag(contentTag); } public Node getGraphic(TagName tagname) { try { if (tagname.equals(getFollowUpTagName())) { - return new ImageView(getFollowUpImage()); + return new ImageView(FOLLOW_UP_IMAGE); } else if (tagname.equals(getBookmarkTagName())) { - return new ImageView(getBookmarkImage()); + return new ImageView(BOOKMARK_IMAGE); } } catch (TskCoreException ex) { - LOGGER.log(Level.SEVERE, "Failed to get \"Follow Up\" or \"Bookmark\"tag name from db.", ex); + logger.log(Level.SEVERE, "Failed to get \"Follow Up\" or \"Bookmark\"tag name from db.", ex); } return DrawableAttribute.TAGS.getGraphicForValue(tagname); } - - synchronized private static Image getFollowUpImage() { - if (FOLLOW_UP_IMAGE == null) { - FOLLOW_UP_IMAGE = new Image("/org/sleuthkit/autopsy/imagegallery/images/flag_red.png"); - } - return FOLLOW_UP_IMAGE; - } - - synchronized private static Image getBookmarkImage() { - if (BOOKMARK_IMAGE == null) { - BOOKMARK_IMAGE = new Image("/org/sleuthkit/autopsy/images/star-bookmark-icon-16.png"); - } - return BOOKMARK_IMAGE; - } - } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/HashSetManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/HashSetManager.java index 9b3b3218f1..19eab6792a 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/HashSetManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/HashSetManager.java @@ -1,8 +1,26 @@ -package org.sleuthkit.autopsy.imagegallery.datamodel; +/* + * Autopsy Forensic Browser + * + * Copyright 2013-18 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */package org.sleuthkit.autopsy.imagegallery.datamodel; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; +import java.sql.SQLException; import java.util.Collections; import java.util.Set; import java.util.logging.Level; @@ -15,26 +33,18 @@ import org.sleuthkit.datamodel.TskCoreException; */ public class HashSetManager { - /** - * The db that initial values are loaded from. - */ - private DrawableDB db = null; + /** The db that initial values are loaded from. */ + private final DrawableDB drawableDB; + + public HashSetManager(DrawableDB drawableDB) { + this.drawableDB = drawableDB; + } /** * the internal cache from fileID to a set of hashset names. */ private final LoadingCache> hashSetCache = CacheBuilder.newBuilder().build(CacheLoader.from(this::getHashSetsForFileHelper)); - /** - * assign the given db to back this hashset manager. - * - * @param db - */ - public void setDb(DrawableDB db) { - this.db = db; - hashSetCache.invalidateAll(); - } - /** * helper method to load hashset hits for the given fileID from the db * @@ -44,9 +54,14 @@ public class HashSetManager { */ private Set getHashSetsForFileHelper(long fileID) { try { - return db.getHashSetsForFile(fileID); - } catch (TskCoreException ex) { - Logger.getLogger(HashSetManager.class.getName()).log(Level.SEVERE, "Failed to get Hash Sets for file", ex); //NON-NLS + if (drawableDB.isClosed()) { + Logger.getLogger(HashSetManager.class.getName()).log(Level.WARNING, "Failed to get Hash Sets for file. The Db connection was already closed."); //NON-NLS + return Collections.emptySet(); + } else { + return drawableDB.getHashSetsForFile(fileID); + } + } catch (TskCoreException | SQLException ex) { + Logger.getLogger(HashSetManager.class.getName()).log(Level.SEVERE, "Failed to get Hash Sets for file."); //NON-NLS return Collections.emptySet(); } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java index 3ef949c9c9..224ac378c6 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-16 Basis Technology Corp. + * Copyright 2013-18 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,16 +26,19 @@ import java.util.logging.Level; import javafx.beans.binding.Bindings; import javafx.beans.binding.DoubleBinding; import javafx.beans.binding.IntegerBinding; +import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyLongProperty; import javafx.beans.property.ReadOnlyLongWrapper; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; +import org.sleuthkit.autopsy.imagegallery.ImageGalleryModule; import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; +import org.sleuthkit.datamodel.TskCoreException; /** * Represents a set of image/video files in a group. The UI listens to changes @@ -76,7 +79,7 @@ public class DrawableGroup implements Comparable { } @SuppressWarnings("ReturnOfCollectionOrArrayField") - public synchronized ObservableList getFileIDs() { + public ObservableList getFileIDs() { return unmodifiableFileIDS; } @@ -121,11 +124,11 @@ public class DrawableGroup implements Comparable { if (hashSetHitsCount.get() < 0) { try { hashSetHitsCount.set(fileIDs.stream() - .map(fileID -> ImageGalleryController.getDefault().getHashSetManager().isInAnyHashSet(fileID)) + .map(ImageGalleryModule.getController().getHashSetManager()::isInAnyHashSet) .filter(Boolean::booleanValue) .count()); - } catch (IllegalStateException | NullPointerException ex) { - LOGGER.log(Level.WARNING, "could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS + } catch (NoCurrentCaseException ex) { + LOGGER.log(Level.WARNING, "Could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS } } return hashSetHitsCount.get(); @@ -139,10 +142,10 @@ public class DrawableGroup implements Comparable { public final synchronized long getUncategorizedCount() { if (uncatCount.get() < 0) { try { - uncatCount.set(ImageGalleryController.getDefault().getDatabase().getUncategorizedCount(fileIDs)); + uncatCount.set(ImageGalleryModule.getController().getDatabase().getUncategorizedCount(fileIDs)); - } catch (IllegalStateException | NullPointerException ex) { - LOGGER.log(Level.WARNING, "could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS + } catch (TskCoreException | NoCurrentCaseException ex) { + LOGGER.log(Level.WARNING, "Could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS } } @@ -163,8 +166,8 @@ public class DrawableGroup implements Comparable { return seen.get(); } - public ReadOnlyBooleanWrapper seenProperty() { - return seen; + public ReadOnlyBooleanProperty seenProperty() { + return seen.getReadOnlyProperty(); } @Subscribe diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java index 9945a72313..8b9cb845d1 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-16 Basis Technology Corp. + * Copyright 2013-18 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,27 +18,31 @@ */ package org.sleuthkit.autopsy.imagegallery.datamodel.grouping; -import java.util.Map; import java.util.Objects; +import java.util.Optional; import javafx.scene.Node; import javax.annotation.concurrent.Immutable; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; +import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.TagName; /** - * key identifying information of a {@link Grouping}. Used to look up groups in - * {@link Map}s and from the db. + * Key identifying information of a DrawableGroup. Used to look up groups in + * Maps and from the db. + * + * @param The type of the values of the attribute this key uses. */ @Immutable public class GroupKey> implements Comparable> { private final T val; - private final DrawableAttribute attr; + private final DataSource dataSource; - public GroupKey(DrawableAttribute attr, T val) { + public GroupKey(DrawableAttribute attr, T val, DataSource dataSource) { this.attr = attr; this.val = val; + this.dataSource = dataSource; } public T getValue() { @@ -49,6 +53,10 @@ public class GroupKey> implements Comparable return attr; } + public Optional< DataSource> getDataSource() { + return Optional.ofNullable(dataSource); + } + public String getValueDisplayName() { return Objects.equals(attr, DrawableAttribute.TAGS) ? ((TagName) getValue()).getDisplayName() @@ -63,13 +71,19 @@ public class GroupKey> implements Comparable @Override public int hashCode() { int hash = 5; - hash = 29 * hash + Objects.hashCode(this.val); - hash = 29 * hash + Objects.hashCode(this.attr); + + hash = 79 * hash + Objects.hashCode(this.val); + hash = 79 * hash + Objects.hashCode(this.attr); + hash = 79 * hash + Objects.hashCode(this.dataSource); + return hash; } @Override public boolean equals(Object obj) { + if (this == obj) { + return true; + } if (obj == null) { return false; } @@ -77,11 +91,14 @@ public class GroupKey> implements Comparable return false; } final GroupKey other = (GroupKey) obj; - if (this.attr != other.attr) { + if (!Objects.equals(this.val, other.val)) { return false; } - return Objects.equals(this.val, other.val); + if (!Objects.equals(this.attr, other.attr)) { + return false; + } + return Objects.equals(this.dataSource, other.dataSource); } @Override @@ -92,4 +109,8 @@ public class GroupKey> implements Comparable public Node getGraphic() { return attr.getGraphicForValue(val); } + + public long getDataSourceObjId() { + return getDataSource().map(DataSource::getId).orElse(0L); + } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java index fb381160f7..95d00ee84a 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-16 Basis Technology Corp. + * Copyright 2013-18 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,10 +18,14 @@ */ package org.sleuthkit.autopsy.imagegallery.datamodel.grouping; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; import com.google.common.eventbus.Subscribe; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -31,33 +35,32 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import static java.util.Objects.nonNull; +import static java.util.Objects.isNull; +import java.util.Optional; import java.util.Set; import java.util.TreeSet; -import java.util.concurrent.ExecutorService; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.regex.Pattern; import java.util.stream.Collectors; -import java.util.stream.Stream; import javafx.application.Platform; +import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyDoubleProperty; -import javafx.beans.property.ReadOnlyDoubleWrapper; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import static javafx.concurrent.Worker.State.CANCELLED; -import static javafx.concurrent.Worker.State.FAILED; -import static javafx.concurrent.Worker.State.READY; -import static javafx.concurrent.Worker.State.RUNNING; -import static javafx.concurrent.Worker.State.SCHEDULED; -import static javafx.concurrent.Worker.State.SUCCEEDED; +import javafx.concurrent.Service; +import javafx.concurrent.Task; +import javafx.concurrent.Worker; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.swing.SortOrder; -import org.apache.commons.lang3.ObjectUtils; +import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; +import static org.apache.commons.lang3.ObjectUtils.notEqual; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.netbeans.api.progress.ProgressHandle; @@ -68,82 +71,71 @@ import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.coreutils.LoggedTask; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.ThreadConfined; -import org.sleuthkit.autopsy.coreutils.ThreadConfined.ThreadType; -import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.datamodel.DhsImageCategory; +import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager; import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.Examiner; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData.DbType; /** - * Provides an abstraction layer on top of {@link DrawableDB} ( and to some - * extent {@link SleuthkitCase} ) to facilitate creation, retrieval, updating, - * and sorting of {@link DrawableGroup}s. + * Provides an abstraction layer on top of DrawableDB ( and to some extent + * SleuthkitCase ) to facilitate creation, retrieval, updating, and sorting of + * DrawableGroups. */ public class GroupManager { - private static final Logger LOGGER = Logger.getLogger(GroupManager.class.getName()); + private static final Logger logger = Logger.getLogger(GroupManager.class.getName()); - private DrawableDB db; + /** An executor to submit async UI related background tasks to. */ + private final ListeningExecutorService exec = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor( + new BasicThreadFactory.Builder().namingPattern("GroupManager BG Thread-%d").build())); //NON-NLS private final ImageGalleryController controller; - /** - * map from {@link GroupKey}s to {@link DrawableGroup}s. All groups (even - * not fully analyzed or not visible groups could be in this map - */ - @GuardedBy("this") - private final Map, DrawableGroup> groupMap = new HashMap<>(); - - /** - * list of all analyzed groups - */ - @ThreadConfined(type = ThreadType.JFX) + /** list of all analyzed groups */ + @GuardedBy("this") //NOPMD private final ObservableList analyzedGroups = FXCollections.observableArrayList(); private final ObservableList unmodifiableAnalyzedGroups = FXCollections.unmodifiableObservableList(analyzedGroups); - /** - * list of unseen groups - */ - @ThreadConfined(type = ThreadType.JFX) + /** list of unseen groups */ + @GuardedBy("this") //NOPMD private final ObservableList unSeenGroups = FXCollections.observableArrayList(); private final ObservableList unmodifiableUnSeenGroups = FXCollections.unmodifiableObservableList(unSeenGroups); - - private ReGroupTask groupByTask; + /** + * map from GroupKey} to DrawableGroupSs. All groups (even not fully + * analyzed or not visible groups could be in this map + */ + @GuardedBy("this") //NOPMD + private final Map, DrawableGroup> groupMap = new HashMap<>(); /* * --- current grouping/sorting attributes --- */ - private volatile GroupSortBy sortBy = GroupSortBy.PRIORITY; - private volatile DrawableAttribute groupBy = DrawableAttribute.PATH; - private volatile SortOrder sortOrder = SortOrder.ASCENDING; + @GuardedBy("this") //NOPMD + private final ReadOnlyObjectWrapper< GroupSortBy> sortByProp = new ReadOnlyObjectWrapper<>(GroupSortBy.PRIORITY); + private final ReadOnlyObjectWrapper< DrawableAttribute> groupByProp = new ReadOnlyObjectWrapper<>(DrawableAttribute.PATH); + private final ReadOnlyObjectWrapper sortOrderProp = new ReadOnlyObjectWrapper<>(SortOrder.ASCENDING); + private final ReadOnlyObjectWrapper dataSourceProp = new ReadOnlyObjectWrapper<>(null);//null indicates all datasources + private final ReadOnlyBooleanWrapper collaborativeModeProp = new ReadOnlyBooleanWrapper(false); - private final ReadOnlyObjectWrapper< Comparator> sortByProp = new ReadOnlyObjectWrapper<>(sortBy); - private final ReadOnlyObjectWrapper< DrawableAttribute> groupByProp = new ReadOnlyObjectWrapper<>(groupBy); - private final ReadOnlyObjectWrapper sortOrderProp = new ReadOnlyObjectWrapper<>(sortOrder); - - private final ReadOnlyDoubleWrapper regroupProgress = new ReadOnlyDoubleWrapper(); - - public void setDB(DrawableDB db) { - this.db = db; - regroup(groupBy, sortBy, sortOrder, Boolean.TRUE); - } + private final GroupingService regrouper; @SuppressWarnings("ReturnOfCollectionOrArrayField") public ObservableList getAnalyzedGroups() { return unmodifiableAnalyzedGroups; } - @ThreadConfined(type = ThreadType.JFX) @SuppressWarnings("ReturnOfCollectionOrArrayField") public ObservableList getUnSeenGroups() { return unmodifiableUnSeenGroups; @@ -152,52 +144,52 @@ public class GroupManager { /** * construct a group manager hooked up to the given db and controller * - * @param db * @param controller */ public GroupManager(ImageGalleryController controller) { this.controller = controller; + this.regrouper = new GroupingService(); + regrouper.setExecutor(exec); } /** - * using the current groupBy set for this manager, find groupkeys for all + * Using the current groupBy set for this manager, find groupkeys for all * the groups the given file is a part of * * @param file * - * @returna a set of {@link GroupKey}s representing the group(s) the given - * file is a part of + * + * @return A a set of GroupKeys representing the group(s) the given file is + * a part of. */ @SuppressWarnings({"rawtypes", "unchecked"}) synchronized public Set> getGroupKeysForFile(DrawableFile file) { Set> resultSet = new HashSet<>(); - for (Comparable val : groupBy.getValue(file)) { - if (groupBy == DrawableAttribute.TAGS) { + for (Comparable val : getGroupBy().getValue(file)) { + if (getGroupBy() == DrawableAttribute.TAGS) { if (CategoryManager.isNotCategoryTagName((TagName) val)) { - resultSet.add(new GroupKey(groupBy, val)); + resultSet.add(new GroupKey(getGroupBy(), val, getDataSource())); } } else { - resultSet.add(new GroupKey(groupBy, val)); + resultSet.add(new GroupKey(getGroupBy(), val, getDataSource())); } } return resultSet; } /** - * using the current groupBy set for this manager, find groupkeys for all - * the groups the given file is a part of + * Using the current grouping paramaters set for this manager, find + * GroupKeys for all the Groups the given file is a part of. * - * @return a a set of {@link GroupKey}s representing the group(s) the given - * file is a part of + * @param fileID The Id of the file to get group keys for. + * + * @return A set of GroupKeys representing the group(s) the given file is a + * part of */ synchronized public Set> getGroupKeysForFileID(Long fileID) { try { - if (nonNull(db)) { - DrawableFile file = db.getFileFromID(fileID); - return getGroupKeysForFile(file); - } else { - Logger.getLogger(GroupManager.class.getName()).log(Level.WARNING, "Failed to load file with id: {0} from database. There is no database assigned.", fileID); //NON-NLS - } + DrawableFile file = getDrawableDB().getFileFromID(fileID); + return getGroupKeysForFile(file); } catch (TskCoreException ex) { Logger.getLogger(GroupManager.class.getName()).log(Level.SEVERE, "failed to load file with id: " + fileID + " from database", ex); //NON-NLS } @@ -211,71 +203,68 @@ public class GroupManager { * or null if no group exists for that key. */ @Nullable - public DrawableGroup getGroupForKey(@Nonnull GroupKey groupKey) { - synchronized (groupMap) { - return groupMap.get(groupKey); - } + synchronized public DrawableGroup getGroupForKey(@Nonnull GroupKey groupKey) { + return groupMap.get(groupKey); } - synchronized public void clear() { + synchronized public void reset() { + Platform.runLater(regrouper::cancel); - if (groupByTask != null) { - groupByTask.cancel(true); - } - sortBy = GroupSortBy.GROUP_BY_VALUE; - groupBy = DrawableAttribute.PATH; - sortOrder = SortOrder.ASCENDING; - Platform.runLater(() -> { - unSeenGroups.forEach(controller.getCategoryManager()::unregisterListener); - unSeenGroups.clear(); - analyzedGroups.forEach(controller.getCategoryManager()::unregisterListener); - analyzedGroups.clear(); + setSortBy(GroupSortBy.GROUP_BY_VALUE); + setGroupBy(DrawableAttribute.PATH); + setSortOrder(SortOrder.ASCENDING); + setDataSource(null); - }); - synchronized (groupMap) { - groupMap.values().forEach(controller.getCategoryManager()::unregisterListener); - groupMap.clear(); - } - db = null; + unSeenGroups.forEach(controller.getCategoryManager()::unregisterListener); + unSeenGroups.clear(); + analyzedGroups.forEach(controller.getCategoryManager()::unregisterListener); + analyzedGroups.clear(); + + groupMap.values().forEach(controller.getCategoryManager()::unregisterListener); + groupMap.clear(); } public boolean isRegrouping() { - if (groupByTask == null) { - return false; - } + Worker.State state = regrouper.getState(); + return Arrays.asList(Worker.State.READY, Worker.State.RUNNING, Worker.State.SCHEDULED) + .contains(state); + } - switch (groupByTask.getState()) { - case READY: - case RUNNING: - case SCHEDULED: - return true; - case CANCELLED: - case FAILED: - - case SUCCEEDED: - default: - return false; - } + public ReadOnlyObjectProperty reGroupingState() { + return regrouper.stateProperty(); } /** - * 'mark' the given group as seen. This removes it from the queue of groups - * to review, and is persisted in the drawable db. + * 'Save' the given group as seen in the drawable db. + * + * @param group The DrawableGroup to mark as seen. + * @param seen The seen state to set for the given group. + * + * @return A ListenableFuture that encapsulates saving the seen state to the + * DB. + * * - * @param group the {@link DrawableGroup} to mark as seen */ - @ThreadConfined(type = ThreadType.JFX) - public void markGroupSeen(DrawableGroup group, boolean seen) { - if (nonNull(db)) { - db.markGroupSeen(group.getGroupKey(), seen); - group.setSeen(seen); - if (seen) { - unSeenGroups.removeAll(group); - } else if (unSeenGroups.contains(group) == false) { - unSeenGroups.add(group); + public ListenableFuture markGroupSeen(DrawableGroup group, boolean seen) { + return exec.submit(() -> { + try { + Examiner examiner = controller.getSleuthKitCase().getCurrentExaminer(); + getDrawableDB().markGroupSeen(group.getGroupKey(), seen, examiner.getId()); + group.setSeen(seen); + updateUnSeenGroups(group); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error marking group as seen", ex); //NON-NLS } - FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy)); + }); + } + + synchronized private void updateUnSeenGroups(DrawableGroup group) { + if (group.isSeen()) { + unSeenGroups.removeAll(group); + } else if (unSeenGroups.contains(group) == false) { + unSeenGroups.add(group); } + sortUnseenGroups(); } /** @@ -285,297 +274,216 @@ public class GroupManager { * * @param groupKey the value of groupKey * @param fileID the value of file + * + * @return The DrawableGroup the file was removed from. + * */ public synchronized DrawableGroup removeFromGroup(GroupKey groupKey, final Long fileID) { //get grouping this file would be in final DrawableGroup group = getGroupForKey(groupKey); if (group != null) { - Platform.runLater(() -> { + synchronized (group) { group.removeFile(fileID); - }); - // If we're grouping by category, we don't want to remove empty groups. - if (groupKey.getAttribute() != DrawableAttribute.CATEGORY) { - if (group.getFileIDs().isEmpty()) { - Platform.runLater(() -> { - if (analyzedGroups.contains(group)) { - analyzedGroups.remove(group); - FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy)); - } - if (unSeenGroups.contains(group)) { - unSeenGroups.remove(group); - FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy)); - } - }); + // If we're grouping by category, we don't want to remove empty groups. + if (groupKey.getAttribute() != DrawableAttribute.CATEGORY + && group.getFileIDs().isEmpty()) { + if (analyzedGroups.contains(group)) { + analyzedGroups.remove(group); + sortAnalyzedGroups(); + } + if (unSeenGroups.contains(group)) { + unSeenGroups.remove(group); + sortUnseenGroups(); + } + } - } else { //group == null - // It may be that this was the last unanalyzed file in the group, so test - // whether the group is now fully analyzed. - popuplateIfAnalyzed(groupKey, null); + return group; } + } else { //group == null + // It may be that this was the last unanalyzed file in the group, so test + // whether the group is now fully analyzed. + return popuplateIfAnalyzed(groupKey, null); } - return group; } - /** - * find the distinct values for the given column (DrawableAttribute) - * - * These values represent the groups of files. - * - * @param groupBy - * - * @return - */ - @SuppressWarnings({"unchecked"}) - public > List findValuesForAttribute(DrawableAttribute groupBy) { - List values = Collections.emptyList(); - try { - switch (groupBy.attrName) { - //these cases get special treatment - case CATEGORY: - values = (List) Arrays.asList(DhsImageCategory.values()); - break; - case TAGS: - values = (List) controller.getTagsManager().getTagNamesInUse().stream() - .filter(CategoryManager::isNotCategoryTagName) - .collect(Collectors.toList()); - break; - case ANALYZED: - values = (List) Arrays.asList(false, true); - break; - case HASHSET: - if (nonNull(db)) { - TreeSet names = new TreeSet<>((Collection) db.getHashSetNames()); - values = new ArrayList<>(names); - } - break; - case MIME_TYPE: - if (nonNull(db)) { - HashSet types = new HashSet<>(); - - // Use the group_concat function to get a list of files for each mime type. - // This has different syntax on Postgres vs SQLite - String groupConcatClause; - if (DbType.POSTGRESQL == controller.getSleuthKitCase().getDatabaseType()) { - groupConcatClause = " array_to_string(array_agg(obj_id), ',') as object_ids"; - } - else { - groupConcatClause = " group_concat(obj_id) as object_ids"; - } - String query = "select " + groupConcatClause + " , mime_type from tsk_files group by mime_type "; - try (SleuthkitCase.CaseDbQuery executeQuery = controller.getSleuthKitCase().executeQuery(query); //NON-NLS - ResultSet resultSet = executeQuery.getResultSet();) { - while (resultSet.next()) { - final String mimeType = resultSet.getString("mime_type"); //NON-NLS - String objIds = resultSet.getString("object_ids"); //NON-NLS - - Pattern.compile(",").splitAsStream(objIds) - .map(Long::valueOf) - .filter(db::isInDB) - .findAny().ifPresent(obj_id -> types.add(mimeType)); - } - } catch (SQLException | TskCoreException ex) { - Exceptions.printStackTrace(ex); - } - values = new ArrayList<>((Collection) types); - } - break; - default: - //otherwise do straight db query - if (nonNull(db)) { - values = db.findValuesForAttribute(groupBy, sortBy, sortOrder); - } - } - - return values; - } catch (TskCoreException ex) { - LOGGER.log(Level.WARNING, "TSK error getting list of type {0}", groupBy.getDisplayName()); //NON-NLS - return Collections.emptyList(); + synchronized private void sortUnseenGroups() { + if (isNotEmpty(unSeenGroups)) { + FXCollections.sort(unSeenGroups, makeGroupComparator(getSortOrder(), getSortBy())); } - } - public Set getFileIDsInGroup(GroupKey groupKey) throws TskCoreException { - Set fileIDsToReturn = Collections.emptySet(); + synchronized private void sortAnalyzedGroups() { + if (isNotEmpty(analyzedGroups)) { + FXCollections.sort(analyzedGroups, makeGroupComparator(getSortOrder(), getSortBy())); + } + } + + synchronized public Set getFileIDsInGroup(GroupKey groupKey) throws TskCoreException { + switch (groupKey.getAttribute().attrName) { //these cases get special treatment case CATEGORY: - fileIDsToReturn = getFileIDsWithCategory((DhsImageCategory) groupKey.getValue()); - break; + return getFileIDsWithCategory((DhsImageCategory) groupKey.getValue()); case TAGS: - fileIDsToReturn = getFileIDsWithTag((TagName) groupKey.getValue()); - break; + return getFileIDsWithTag((TagName) groupKey.getValue()); case MIME_TYPE: - fileIDsToReturn = getFileIDsWithMimeType((String) groupKey.getValue()); - break; + return getFileIDsWithMimeType((String) groupKey.getValue()); // case HASHSET: //comment out this case to use db functionality for hashsets // return getFileIDsWithHashSetName((String) groupKey.getValue()); default: //straight db query - if (nonNull(db)) { - fileIDsToReturn = db.getFileIDsInGroup(groupKey); - } + return getDrawableDB().getFileIDsInGroup(groupKey); } - return fileIDsToReturn; } // @@@ This was kind of slow in the profiler. Maybe we should cache it. // Unless the list of file IDs is necessary, use countFilesWithCategory() to get the counts. - public Set getFileIDsWithCategory(DhsImageCategory category) throws TskCoreException { + synchronized public Set getFileIDsWithCategory(DhsImageCategory category) throws TskCoreException { Set fileIDsToReturn = Collections.emptySet(); - if (nonNull(db)) { - try { - final DrawableTagsManager tagsManager = controller.getTagsManager(); - if (category == DhsImageCategory.ZERO) { - List< TagName> tns = Stream.of(DhsImageCategory.ONE, DhsImageCategory.TWO, DhsImageCategory.THREE, DhsImageCategory.FOUR, DhsImageCategory.FIVE) - .map(tagsManager::getTagName) - .collect(Collectors.toList()); - Set files = new HashSet<>(); - for (TagName tn : tns) { - if (tn != null) { - List contentTags = tagsManager.getContentTagsByTagName(tn); - files.addAll(contentTags.stream() - .filter(ct -> ct.getContent() instanceof AbstractFile) - .filter(ct -> db.isInDB(ct.getContent().getId())) - .map(ct -> ct.getContent().getId()) - .collect(Collectors.toSet())); - } + try { + final DrawableTagsManager tagsManager = controller.getTagsManager(); + if (category == DhsImageCategory.ZERO) { + Set fileIDs = new HashSet<>(); + for (TagName catTagName : tagsManager.getCategoryTagNames()) { + if (notEqual(catTagName.getDisplayName(), DhsImageCategory.ZERO.getDisplayName())) { + tagsManager.getContentTagsByTagName(catTagName).stream() + .filter(ct -> ct.getContent() instanceof AbstractFile) + .map(ct -> ct.getContent().getId()) + .filter(getDrawableDB()::isInDB) + .forEach(fileIDs::add); } - - fileIDsToReturn = db.findAllFileIdsWhere("obj_id NOT IN (" + StringUtils.join(files, ',') + ")"); //NON-NLS - } else { - - List contentTags = tagsManager.getContentTagsByTagName(tagsManager.getTagName(category)); - fileIDsToReturn = contentTags.stream() - .filter(ct -> ct.getContent() instanceof AbstractFile) - .filter(ct -> db.isInDB(ct.getContent().getId())) - .map(ct -> ct.getContent().getId()) - .collect(Collectors.toSet()); } - } catch (TskCoreException ex) { - LOGGER.log(Level.WARNING, "TSK error getting files in Category:" + category.getDisplayName(), ex); //NON-NLS - throw ex; + + fileIDsToReturn = getDrawableDB().findAllFileIdsWhere("obj_id NOT IN (" + StringUtils.join(fileIDs, ',') + ")"); //NON-NLS + } else { + + List contentTags = tagsManager.getContentTagsByTagName(tagsManager.getTagName(category)); + fileIDsToReturn = contentTags.stream() + .filter(ct -> ct.getContent() instanceof AbstractFile) + .filter(ct -> getDrawableDB().isInDB(ct.getContent().getId())) + .map(ct -> ct.getContent().getId()) + .collect(Collectors.toSet()); } + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "TSK error getting files in Category:" + category.getDisplayName(), ex); //NON-NLS + throw ex; } + return fileIDsToReturn; } - public Set getFileIDsWithTag(TagName tagName) throws TskCoreException { - try { - Set files = new HashSet<>(); - List contentTags = controller.getTagsManager().getContentTagsByTagName(tagName); - for (ContentTag ct : contentTags) { - if (ct.getContent() instanceof AbstractFile && nonNull(db) && db.isInDB(ct.getContent().getId())) { - files.add(ct.getContent().getId()); - } - } - return files; - } catch (TskCoreException ex) { - LOGGER.log(Level.WARNING, "TSK error getting files with Tag:" + tagName.getDisplayName(), ex); //NON-NLS - throw ex; - } + synchronized public Set getFileIDsWithTag(TagName tagName) throws TskCoreException { + return controller.getTagsManager().getContentTagsByTagName(tagName).stream() + .map(ContentTag::getContent) + .filter(AbstractFile.class::isInstance) + .map(Content::getId) + .filter(getDrawableDB()::isInDB) + .collect(Collectors.toSet()); } - public GroupSortBy getSortBy() { - return sortBy; + public synchronized GroupSortBy getSortBy() { + return sortByProp.get(); } - void setSortBy(GroupSortBy sortBy) { - this.sortBy = sortBy; - Platform.runLater(() -> sortByProp.set(sortBy)); + synchronized void setSortBy(GroupSortBy sortBy) { + sortByProp.set(sortBy); } - public ReadOnlyObjectProperty< Comparator> getSortByProperty() { + public ReadOnlyObjectProperty< GroupSortBy> getSortByProperty() { return sortByProp.getReadOnlyProperty(); } - public DrawableAttribute getGroupBy() { - return groupBy; + public synchronized DrawableAttribute getGroupBy() { + return groupByProp.get(); } - void setGroupBy(DrawableAttribute groupBy) { - this.groupBy = groupBy; - Platform.runLater(() -> groupByProp.set(groupBy)); + synchronized void setGroupBy(DrawableAttribute groupBy) { + groupByProp.set(groupBy); } public ReadOnlyObjectProperty> getGroupByProperty() { return groupByProp.getReadOnlyProperty(); } - public SortOrder getSortOrder() { - return sortOrder; + public synchronized SortOrder getSortOrder() { + return sortOrderProp.get(); } - void setSortOrder(SortOrder sortOrder) { - this.sortOrder = sortOrder; - Platform.runLater(() -> sortOrderProp.set(sortOrder)); + synchronized void setSortOrder(SortOrder sortOrder) { + sortOrderProp.set(sortOrder); } public ReadOnlyObjectProperty getSortOrderProperty() { return sortOrderProp.getReadOnlyProperty(); } + public synchronized DataSource getDataSource() { + return dataSourceProp.get(); + } + + synchronized void setDataSource(DataSource dataSource) { + dataSourceProp.set(dataSource); + } + + public ReadOnlyObjectProperty getDataSourceProperty() { + return dataSourceProp.getReadOnlyProperty(); + } + /** - * regroup all files in the database using given {@link DrawableAttribute} - * see {@link ReGroupTask} for more details. + * Regroup all files in the database. see ReGroupTask for more details. * - * @param groupBy - * @param sortBy - * @param sortOrder - * @param force true to force a full db query regroup + * @param The type of the values of the groupBy attriubte. + * @param dataSource The DataSource to show. Null for all data sources. + * @param groupBy The DrawableAttribute to group by + * @param sortBy The GroupSortBy to sort the groups by + * @param sortOrder The SortOrder to use when sorting the groups. + * @param force true to force a full db query regroup, even if only the + * sorting has changed. */ - public synchronized > void regroup(final DrawableAttribute groupBy, final GroupSortBy sortBy, final SortOrder sortOrder, Boolean force) { + public synchronized > void regroup(DataSource dataSource, DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder, Boolean force) { if (!Case.isCaseOpen()) { return; } - //only re-query the db if the group by attribute changed or it is forced - if (groupBy != getGroupBy() || force == true) { + //only re-query the db if the data source or group by attribute changed or it is forced + if (dataSource != getDataSource() + || groupBy != getGroupBy() + || force) { + + setDataSource(dataSource); setGroupBy(groupBy); setSortBy(sortBy); setSortOrder(sortOrder); - if (groupByTask != null) { - groupByTask.cancel(true); - } - - groupByTask = new ReGroupTask<>(groupBy, sortBy, sortOrder); - Platform.runLater(() -> regroupProgress.bind(groupByTask.progressProperty())); - regroupExecutor.submit(groupByTask); + Platform.runLater(regrouper::restart); } else { // resort the list of groups setSortBy(sortBy); setSortOrder(sortOrder); - Platform.runLater(() -> { - FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy)); - FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy)); - }); + sortAnalyzedGroups(); + sortUnseenGroups(); } } - /** - * an executor to submit async ui related background tasks to. - */ - final ExecutorService regroupExecutor = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder().namingPattern("ui task -%d").build()); //NON-NLS - public ReadOnlyDoubleProperty regroupProgress() { - return regroupProgress.getReadOnlyProperty(); + return regrouper.progressProperty(); } @Subscribe - public void handleTagAdded(ContentTagAddedEvent evt) { + synchronized public void handleTagAdded(ContentTagAddedEvent evt) { GroupKey newGroupKey = null; final long fileID = evt.getAddedTag().getContent().getId(); - if (groupBy == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(evt.getAddedTag().getName())) { - newGroupKey = new GroupKey<>(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(evt.getAddedTag().getName())); + if (getGroupBy() == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(evt.getAddedTag().getName())) { + newGroupKey = new GroupKey<>(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(evt.getAddedTag().getName()), getDataSource()); for (GroupKey oldGroupKey : groupMap.keySet()) { if (oldGroupKey.equals(newGroupKey) == false) { removeFromGroup(oldGroupKey, fileID); } } - } else if (groupBy == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(evt.getAddedTag().getName())) { - newGroupKey = new GroupKey<>(DrawableAttribute.TAGS, evt.getAddedTag().getName()); + } else if (getGroupBy() == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(evt.getAddedTag().getName())) { + newGroupKey = new GroupKey<>(DrawableAttribute.TAGS, evt.getAddedTag().getName(), getDataSource()); } if (newGroupKey != null) { DrawableGroup g = getGroupForKey(newGroupKey); @@ -584,27 +492,26 @@ public class GroupManager { } @SuppressWarnings("AssignmentToMethodParameter") - private void addFileToGroup(DrawableGroup g, final GroupKey groupKey, final long fileID) { - if (g == null) { + synchronized private void addFileToGroup(DrawableGroup group, final GroupKey groupKey, final long fileID) { + if (group == null) { //if there wasn't already a group check if there should be one now - g = popuplateIfAnalyzed(groupKey, null); + group = popuplateIfAnalyzed(groupKey, null); } - DrawableGroup group = g; if (group != null) { //if there is aleady a group that was previously deemed fully analyzed, then add this newly analyzed file to it. - Platform.runLater(() -> group.addFile(fileID)); + group.addFile(fileID); } } @Subscribe - public void handleTagDeleted(ContentTagDeletedEvent evt) { + synchronized public void handleTagDeleted(ContentTagDeletedEvent evt) { GroupKey groupKey = null; final ContentTagDeletedEvent.DeletedContentTagInfo deletedTagInfo = evt.getDeletedTagInfo(); final TagName tagName = deletedTagInfo.getName(); - if (groupBy == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(tagName)) { - groupKey = new GroupKey<>(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(tagName)); - } else if (groupBy == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(tagName)) { - groupKey = new GroupKey<>(DrawableAttribute.TAGS, tagName); + if (getGroupBy() == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(tagName)) { + groupKey = new GroupKey<>(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(tagName), getDataSource()); + } else if (getGroupBy() == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(tagName)) { + groupKey = new GroupKey<>(DrawableAttribute.TAGS, tagName, getDataSource()); } if (groupKey != null) { final long fileID = deletedTagInfo.getContentID(); @@ -626,10 +533,9 @@ public class GroupManager { } /** - * handle {@link FileUpdateEvent} sent from Db when files are - * inserted/updated + * Handle notifications sent from Db when files are inserted/updated * - * @param evt + * @param updatedFileIDs The ID of the inserted/updated files. */ @Subscribe synchronized public void handleFileUpdate(Collection updatedFileIDs) { @@ -654,67 +560,60 @@ public class GroupManager { controller.getCategoryManager().fireChange(updatedFileIDs, null); } - private DrawableGroup popuplateIfAnalyzed(GroupKey groupKey, ReGroupTask task) { + synchronized private DrawableGroup popuplateIfAnalyzed(GroupKey groupKey, ReGroupTask task) { + /* + * If this method call is part of a ReGroupTask and that task is + * cancelled, no-op. + * + * This allows us to stop if a regroup task has been cancelled (e.g. the + * user picked a different group by attribute, while the current task + * was still running) + */ + if (isNull(task) || task.isCancelled() == false) { - if (Objects.nonNull(task) && (task.isCancelled())) { /* - * if this method call is part of a ReGroupTask and that task is - * cancelled, no-op - * - * this allows us to stop if a regroup task has been cancelled (e.g. - * the user picked a different group by attribute, while the current - * task was still running) + * For attributes other than path we can't be sure a group is fully + * analyzed because we don't know all the files that will be a part + * of that group. just show them no matter what. */ - - } else // no task or un-cancelled task - { - if (nonNull(db) && ((groupKey.getAttribute() != DrawableAttribute.PATH) || db.isGroupAnalyzed(groupKey))) { - /* - * for attributes other than path we can't be sure a group is - * fully analyzed because we don't know all the files that will - * be a part of that group,. just show them no matter what. - */ - + if (groupKey.getAttribute() != DrawableAttribute.PATH + || getDrawableDB().isGroupAnalyzed(groupKey)) { try { Set fileIDs = getFileIDsInGroup(groupKey); if (Objects.nonNull(fileIDs)) { + + long examinerID = collaborativeModeProp.get() ? -1 : controller.getSleuthKitCase().getCurrentExaminer().getId(); + final boolean groupSeen = getDrawableDB().isGroupSeenByExaminer(groupKey, examinerID); DrawableGroup group; - final boolean groupSeen = db.isGroupSeen(groupKey); - synchronized (groupMap) { - if (groupMap.containsKey(groupKey)) { - group = groupMap.get(groupKey); - group.setFiles(ObjectUtils.defaultIfNull(fileIDs, Collections.emptySet())); - } else { - group = new DrawableGroup(groupKey, fileIDs, groupSeen); - controller.getCategoryManager().registerListener(group); - group.seenProperty().addListener((o, oldSeen, newSeen) -> - Platform.runLater(() -> markGroupSeen(group, newSeen)) - ); - groupMap.put(groupKey, group); - } + if (groupMap.containsKey(groupKey)) { + group = groupMap.get(groupKey); + group.setFiles(fileIDs); + group.setSeen(groupSeen); + } else { + group = new DrawableGroup(groupKey, fileIDs, groupSeen); + controller.getCategoryManager().registerListener(group); + groupMap.put(groupKey, group); } - Platform.runLater(() -> { - if (analyzedGroups.contains(group) == false) { - analyzedGroups.add(group); - if (Objects.isNull(task)) { - FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy)); - } - } - markGroupSeen(group, groupSeen); - }); - return group; + if (analyzedGroups.contains(group) == false) { + analyzedGroups.add(group); + sortAnalyzedGroups(); + } + updateUnSeenGroups(group); + + return group; } } catch (TskCoreException ex) { - LOGGER.log(Level.SEVERE, "failed to get files for group: " + groupKey.getAttribute().attrName.toString() + " = " + groupKey.getValue(), ex); //NON-NLS + logger.log(Level.SEVERE, "failed to get files for group: " + groupKey.getAttribute().attrName.toString() + " = " + groupKey.getValue(), ex); //NON-NLS } } } + return null; } - public Set getFileIDsWithMimeType(String mimeType) throws TskCoreException { + synchronized public Set getFileIDsWithMimeType(String mimeType) throws TskCoreException { HashSet hashSet = new HashSet<>(); String query = (null == mimeType) @@ -725,21 +624,45 @@ public class GroupManager { ResultSet resultSet = executeQuery.getResultSet();) { while (resultSet.next()) { final long fileID = resultSet.getLong("obj_id"); //NON-NLS - if (nonNull(db) && db.isInDB(fileID)) { + if (getDrawableDB().isInDB(fileID)) { hashSet.add(fileID); } } return hashSet; } catch (Exception ex) { - Exceptions.printStackTrace(ex); throw new TskCoreException("Failed to get file ids with mime type " + mimeType, ex); } } + synchronized public void setCollaborativeMode(Boolean newValue) { + collaborativeModeProp.set(newValue); + analyzedGroups.forEach(group -> { + try { + boolean groupSeenByExaminer = getDrawableDB().isGroupSeenByExaminer( + group.getGroupKey(), + newValue ? -1 : controller.getSleuthKitCase().getCurrentExaminer().getId() + ); + group.setSeen(groupSeenByExaminer); + updateUnSeenGroups(group); + if (group.isSeen()) { + unSeenGroups.removeAll(group); + } else if (unSeenGroups.contains(group) == false) { + unSeenGroups.add(group); + } + + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error checking seen state of group.", ex); + } + }); + sortUnseenGroups(); + } + /** * Task to query database for files in sorted groups and build - * {@link Groupings} for them + * DrawableGroups for them. + * + * @param The type of the values that this task will group by. */ @SuppressWarnings({"unchecked", "rawtypes"}) @NbBundle.Messages({"# {0} - groupBy attribute Name", @@ -749,76 +672,192 @@ public class GroupManager { "# {0} - groupBy attribute Name", "# {1} - atribute value", "ReGroupTask.progressUpdate=regrouping files by {0} : {1}"}) - private class ReGroupTask> extends LoggedTask { - - private ProgressHandle groupProgress; - - private final DrawableAttribute groupBy; + class ReGroupTask> extends LoggedTask { + private final DataSource dataSource; + private final DrawableAttribute groupBy; private final GroupSortBy sortBy; - private final SortOrder sortOrder; - ReGroupTask(DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder) { - super(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), true); + private final ProgressHandle groupProgress; + ReGroupTask(DataSource dataSource, DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder) { + super(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), true); + this.dataSource = dataSource; this.groupBy = groupBy; this.sortBy = sortBy; this.sortOrder = sortOrder; - } - @Override - public boolean isCancelled() { - return super.isCancelled() || groupBy != getGroupBy() || sortBy != getSortBy() || sortOrder != getSortOrder(); + groupProgress = ProgressHandle.createHandle(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), this); } @Override protected Void call() throws Exception { + try { + if (isCancelled()) { + return null; + } + groupProgress.start(); - if (isCancelled()) { - return null; - } - - groupProgress = ProgressHandle.createHandle(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), this); - Platform.runLater(() -> { analyzedGroups.clear(); unSeenGroups.clear(); - }); - // Get the list of group keys - final List vals = findValuesForAttribute(groupBy); + // Get the list of group keys + Multimap valsByDataSource = findValuesForAttribute(); - groupProgress.start(vals.size()); - - int p = 0; - // For each key value, partially create the group and add it to the list. - for (final AttrType val : vals) { - if (isCancelled()) { - return null;//abort + groupProgress.switchToDeterminate(valsByDataSource.entries().size()); + int p = 0; + // For each key value, partially create the group and add it to the list. + for (final Map.Entry val : valsByDataSource.entries()) { + if (isCancelled()) { + return null; + } + p++; + updateMessage(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val.getValue())); + updateProgress(p, valsByDataSource.size()); + groupProgress.progress(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val), p); + popuplateIfAnalyzed(new GroupKey<>(groupBy, val.getValue(), val.getKey()), this); } - p++; - updateMessage(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val)); - updateProgress(p, vals.size()); - groupProgress.progress(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val), p); - popuplateIfAnalyzed(new GroupKey<>(groupBy, val), this); - } - Platform.runLater(() -> FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy))); - updateProgress(1, 1); + Optional viewedGroup + = Optional.ofNullable(controller.getViewState()) + .flatMap(GroupViewState::getGroup); + Optional> viewedKey = viewedGroup.map(DrawableGroup::getGroupKey); + DataSource dataSourceOfCurrentGroup + = viewedKey.flatMap(GroupKey::getDataSource) + .orElse(null); + DrawableAttribute attributeOfCurrentGroup + = viewedKey.map(GroupKey::getAttribute) + .orElse(null); + /* if no group or if groupbies are different or if data source + * != null and does not equal group */ + if (viewedGroup.isPresent() == false) { + + //the current group should not be visible so ... + if (isNotEmpty(unSeenGroups)) {// show then next unseen group + controller.advance(GroupViewState.tile(unSeenGroups.get(0))); + } else if (isNotEmpty(analyzedGroups)) { + //show the first analyzed group. + controller.advance(GroupViewState.tile(analyzedGroups.get(0))); + } else { //there are no groups, clear the group area. + controller.advance(GroupViewState.tile(null)); + } + } else if ((getDataSource() != null && notEqual(dataSourceOfCurrentGroup, getDataSource()))) { + + //the current group should not be visible so ... + if (isNotEmpty(unSeenGroups)) {// show then next unseen group + controller.advance(GroupViewState.tile(unSeenGroups.get(0))); + } else if (isNotEmpty(analyzedGroups)) { + //show the first analyzed group. + controller.advance(GroupViewState.tile(analyzedGroups.get(0))); + } else { //there are no groups, clear the group area. + controller.advance(GroupViewState.tile(null)); + } + } else if (getGroupBy() != attributeOfCurrentGroup) { + //the current group should not be visible so ... + if (isNotEmpty(unSeenGroups)) {// show then next unseen group + controller.advance(GroupViewState.tile(unSeenGroups.get(0))); + } else if (isNotEmpty(analyzedGroups)) { + //show the first analyzed group. + controller.advance(GroupViewState.tile(analyzedGroups.get(0))); + } else { //there are no groups, clear the group area. + controller.advance(GroupViewState.tile(null)); + } + } + } finally { + groupProgress.finish(); + updateProgress(1, 1); + } return null; } @Override protected void done() { super.done(); - if (groupProgress != null) { - groupProgress.finish(); - groupProgress = null; + try { + get(); + } catch (CancellationException cancelEx) { //NOPMD + //cancellation is normal + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.SEVERE, "Error while regrouping.", ex); } } + + /** + * find the distinct values for the given column (DrawableAttribute) + * + * These values represent the groups of files. + * + * @param groupBy + * + * @return + */ + public Multimap findValuesForAttribute() { + + Multimap results = HashMultimap.create(); + try { + switch (groupBy.attrName) { + //these cases get special treatment + case CATEGORY: + results.putAll(null, Arrays.asList(DhsImageCategory.values())); + break; + case TAGS: + results.putAll(null, controller.getTagsManager().getTagNamesInUse().stream() + .filter(CategoryManager::isNotCategoryTagName) + .collect(Collectors.toList())); + break; + + case ANALYZED: + results.putAll(null, Arrays.asList(false, true)); + break; + case HASHSET: + + results.putAll(null, new TreeSet<>(getDrawableDB().getHashSetNames())); + + break; + case MIME_TYPE: + + HashSet types = new HashSet<>(); + + // Use the group_concat function to get a list of files for each mime type. + // This has different syntax on Postgres vs SQLite + String groupConcatClause; + if (DbType.POSTGRESQL == controller.getSleuthKitCase().getDatabaseType()) { + groupConcatClause = " array_to_string(array_agg(obj_id), ',') as object_ids"; + } else { + groupConcatClause = " group_concat(obj_id) as object_ids"; + } + String query = "select " + groupConcatClause + " , mime_type from tsk_files group by mime_type "; + try (SleuthkitCase.CaseDbQuery executeQuery = controller.getSleuthKitCase().executeQuery(query); //NON-NLS + ResultSet resultSet = executeQuery.getResultSet();) { + while (resultSet.next()) { + final String mimeType = resultSet.getString("mime_type"); //NON-NLS + String objIds = resultSet.getString("object_ids"); //NON-NLS + + Pattern.compile(",").splitAsStream(objIds) + .map(Long::valueOf) + .filter(getDrawableDB()::isInDB) + .findAny().ifPresent(obj_id -> types.add(mimeType)); + } + } catch (SQLException | TskCoreException ex) { + Exceptions.printStackTrace(ex); + } + results.putAll(null, types); + + break; + default: + //otherwise do straight db query + results.putAll(getDrawableDB().findValuesForAttribute(groupBy, sortBy, sortOrder, dataSource)); + } + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "TSK error getting list of type {0}", groupBy.getDisplayName()); //NON-NLS + } + return results; + + } } - private static Comparator applySortOrder(final SortOrder sortOrder, Comparator comparator) { + private static Comparator makeGroupComparator(final SortOrder sortOrder, GroupSortBy comparator) { switch (sortOrder) { case ASCENDING: return comparator; @@ -829,4 +868,21 @@ public class GroupManager { return new GroupSortBy.AllEqualComparator<>(); } } + + /** + * @return the drawableDB + */ + private DrawableDB getDrawableDB() { + return controller.getDatabase(); + } + + class GroupingService extends Service< Void> { + + @Override + protected Task createTask() { + synchronized (GroupManager.this) { + return new ReGroupTask<>(getDataSource(), getGroupBy(), getSortBy(), getSortOrder()); + } + } + } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java index 65e1870de8..97a75f0f5b 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java @@ -28,7 +28,8 @@ import org.openide.util.NbBundle; /** * Pseudo enum of possible properties to sort groups by. */ -@NbBundle.Messages({"GroupSortBy.groupSize=Group Size", +@NbBundle.Messages({ + "GroupSortBy.groupSize=Group Size", "GroupSortBy.groupName=Group Name", "GroupSortBy.none=None", "GroupSortBy.priority=Priority"}) @@ -37,40 +38,35 @@ public class GroupSortBy implements Comparator { /** * sort the groups by the number of files in each */ - public final static GroupSortBy FILE_COUNT = - new GroupSortBy(Bundle.GroupSortBy_groupSize(), "folder-open-image.png", + public final static GroupSortBy FILE_COUNT + = new GroupSortBy(Bundle.GroupSortBy_groupSize(), "folder-open-image.png", Comparator.comparing(DrawableGroup::getSize)); /** * sort the groups by the natural order of the grouping value ( eg group * them by path alphabetically ) */ - public final static GroupSortBy GROUP_BY_VALUE = - new GroupSortBy(Bundle.GroupSortBy_groupName(), "folder-rename.png", + public final static GroupSortBy GROUP_BY_VALUE + = new GroupSortBy(Bundle.GroupSortBy_groupName(), "folder-rename.png", Comparator.comparing(DrawableGroup::getGroupByValueDislpayName)); /** * don't sort the groups just use what ever order they come in (ingest * order) */ - public final static GroupSortBy NONE = - new GroupSortBy(Bundle.GroupSortBy_none(), "prohibition.png", + public final static GroupSortBy NONE + = new GroupSortBy(Bundle.GroupSortBy_none(), "prohibition.png", new AllEqualComparator<>()); /** * sort the groups by some priority metric to be determined and implemented */ - public final static GroupSortBy PRIORITY = - new GroupSortBy(Bundle.GroupSortBy_priority(), "hashset_hits.png", + public final static GroupSortBy PRIORITY + = new GroupSortBy(Bundle.GroupSortBy_priority(), "hashset_hits.png", Comparator.comparing(DrawableGroup::getHashHitDensity) .thenComparing(Comparator.comparing(DrawableGroup::getUncategorizedCount)) .reversed()); - @Override - public int compare(DrawableGroup o1, DrawableGroup o2) { - return delegate.compare(o1, o2); - } - private final static ObservableList values = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(PRIORITY, NONE, GROUP_BY_VALUE, FILE_COUNT)); /** @@ -109,6 +105,11 @@ public class GroupSortBy implements Comparator { return icon; } + @Override + public int compare(DrawableGroup o1, DrawableGroup o2) { + return delegate.compare(o1, o2); + } + static class AllEqualComparator implements Comparator { @Override diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupViewState.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupViewState.java index 86679340ab..fa578021ef 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupViewState.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupViewState.java @@ -22,9 +22,9 @@ import java.util.Objects; import java.util.Optional; /** - * + * Encapsulate information about the state of the group section of the UI. */ -public class GroupViewState { +public final class GroupViewState { private final DrawableGroup group; @@ -32,8 +32,8 @@ public class GroupViewState { private final Optional slideShowfileID; - public DrawableGroup getGroup() { - return group; + public Optional getGroup() { + return Optional.ofNullable(group); } public GroupViewMode getMode() { @@ -44,18 +44,18 @@ public class GroupViewState { return slideShowfileID; } - private GroupViewState(DrawableGroup g, GroupViewMode mode, Long slideShowfileID) { - this.group = g; + private GroupViewState(DrawableGroup group, GroupViewMode mode, Long slideShowfileID) { + this.group = group; this.mode = mode; this.slideShowfileID = Optional.ofNullable(slideShowfileID); } - public static GroupViewState tile(DrawableGroup g) { - return new GroupViewState(g, GroupViewMode.TILE, null); + public static GroupViewState tile(DrawableGroup group) { + return new GroupViewState(group, GroupViewMode.TILE, null); } - public static GroupViewState slideShow(DrawableGroup g, Long fileID) { - return new GroupViewState(g, GroupViewMode.SLIDE_SHOW, fileID); + public static GroupViewState slideShow(DrawableGroup group, Long fileID) { + return new GroupViewState(group, GroupViewMode.SLIDE_SHOW, fileID); } @Override @@ -82,10 +82,7 @@ public class GroupViewState { if (this.mode != other.mode) { return false; } - if (!Objects.equals(this.slideShowfileID, other.slideShowfileID)) { - return false; - } - return true; + return Objects.equals(this.slideShowfileID, other.slideShowfileID); } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/GuiUtils.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/GuiUtils.java index 2ff6f24d90..91b55e5bf1 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/GuiUtils.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/GuiUtils.java @@ -18,20 +18,43 @@ */ package org.sleuthkit.autopsy.imagegallery.gui; +import java.io.IOException; +import java.net.URL; +import java.util.logging.Level; import javafx.scene.control.ButtonBase; +import javafx.scene.control.Dialog; import javafx.scene.control.MenuItem; +import javafx.scene.image.Image; +import javafx.stage.Stage; import org.controlsfx.control.action.Action; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; /** * Static utility methods for working with GUI components */ -public class GuiUtils { +public final class GuiUtils { + + private final static Logger logger = Logger.getLogger(GuiUtils.class.getName()); + + /** Image to use as title bar icon in dialogs */ + private static final Image AUTOPSY_ICON; + + static { + Image tempImg = null; + try { + tempImg = new Image(new URL("nbresloc:/org/netbeans/core/startup/frame.gif").openStream()); //NON-NLS + } catch (IOException ex) { + logger.log(Level.WARNING, "Failed to load branded icon for progress dialog.", ex); //NON-NLS + } + AUTOPSY_ICON = tempImg; + } private GuiUtils() { } /** - * create a MenuItem that performs the given action and also set the Action + * Create a MenuItem that performs the given action and also set the Action * as the action for the given Button. Usefull to have a SplitMenuButton * remember the last chosen menu item as its action. * @@ -51,4 +74,14 @@ public class GuiUtils { }); return menuItem; } + + /** + * Set the title bar icon for the given Dialog to be the Autopsy logo icon. + * + * @param dialog The dialog to set the title bar icon for. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + public static void setDialogIcons(Dialog dialog) { + ((Stage) dialog.getDialogPane().getScene().getWindow()).getIcons().setAll(AUTOPSY_ICON); + } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/StatusBar.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/StatusBar.java index 143ef142fd..2355d6d2a0 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/StatusBar.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/StatusBar.java @@ -79,7 +79,7 @@ public class StatusBar extends AnchorPane { }); Platform.runLater(() -> staleLabel.setTooltip(new Tooltip(Bundle.StatuBar_toolTip()))); - staleLabel.visibleProperty().bind(controller.stale()); + staleLabel.visibleProperty().bind(controller.staleProperty()); } public StatusBar(ImageGalleryController controller) { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java index 44814e2c9a..a9c4da2907 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-15 Basis Technology Corp. + * Copyright 2013-18 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,9 +34,10 @@ import javafx.scene.layout.VBox; import javafx.util.Pair; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.datamodel.DhsImageCategory; import org.sleuthkit.autopsy.imagegallery.FXMLConstructor; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; -import org.sleuthkit.autopsy.datamodel.DhsImageCategory; +import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager.CategoryChangeEvent; /** * Displays summary statistics (counts) for each group @@ -51,11 +52,13 @@ public class SummaryTablePane extends AnchorPane { @FXML private TableView> tableView; + private final ImageGalleryController controller; @FXML - @NbBundle.Messages({"SummaryTablePane.catColumn=Category", - "SummaryTablePane.countColumn=# Files"}) + @NbBundle.Messages({ + "SummaryTablePane.catColumn=Category", + "SummaryTablePane.countColumn=# Files"}) void initialize() { assert catColumn != null : "fx:id=\"catColumn\" was not injected: check your FXML file 'SummaryTablePane.fxml'."; assert countColumn != null : "fx:id=\"countColumn\" was not injected: check your FXML file 'SummaryTablePane.fxml'."; @@ -67,11 +70,11 @@ public class SummaryTablePane extends AnchorPane { tableView.prefHeightProperty().set(7 * 25); //set up columns - catColumn.setCellValueFactory((TableColumn.CellDataFeatures, String> p) -> new SimpleObjectProperty<>(p.getValue().getKey().getDisplayName())); + catColumn.setCellValueFactory(params -> new SimpleObjectProperty<>(params.getValue().getKey().getDisplayName())); catColumn.setPrefWidth(USE_COMPUTED_SIZE); catColumn.setText(Bundle.SummaryTablePane_catColumn()); - countColumn.setCellValueFactory((TableColumn.CellDataFeatures, Long> p) -> new SimpleObjectProperty<>(p.getValue().getValue())); + countColumn.setCellValueFactory(params -> new SimpleObjectProperty<>(params.getValue().getValue())); countColumn.setPrefWidth(USE_COMPUTED_SIZE); countColumn.setText(Bundle.SummaryTablePane_countColumn()); @@ -85,14 +88,15 @@ public class SummaryTablePane extends AnchorPane { public SummaryTablePane(ImageGalleryController controller) { this.controller = controller; FXMLConstructor.construct(this, "SummaryTablePane.fxml"); //NON-NLS - } /** * listen to Category updates and rebuild the table + * + * @param evt The change event. */ @Subscribe - public void handleCategoryChanged(org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager.CategoryChangeEvent evt) { + public void handleCategoryChanged(CategoryChangeEvent evt) { final ObservableList> data = FXCollections.observableArrayList(); if (Case.isCaseOpen()) { for (DhsImageCategory cat : DhsImageCategory.values()) { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.fxml b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.fxml index a8ced6deda..77f73b2cb9 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.fxml +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.fxml @@ -12,29 +12,41 @@ - - + + - - + + + + + + + + + + + + + - - - - - - + - +