Merge remote-tracking branch 'upstream/develop' into 2823_tskLayerTest

This commit is contained in:
Ann Priestman
2018-06-08 11:30:43 -04:00
77 changed files with 3986 additions and 561 deletions
+8 -4
View File
@@ -89,22 +89,26 @@
<target name="getTestDataFiles">
<mkdir dir="${basedir}/test/qa-functional/data"/>
<get src="https://drive.google.com/uc?id=1dLYGctuvRQMmnzfXPppTM_9gB49eLc_g" dest="${test-input}/embedded.vhd" skipexisting="true"/>
<get src="https://drive.google.com/uc?id=1dLYGctuvRQMmnzfXPppTM_9gB49eLc_g" dest="${test-input}/embedded.vhd" skipexisting="true"/>
<get src="https://drive.google.com/uc?id=1JACMDyH4y54ypGzFWl82ZzMQf3qbrioP" dest="${test-input}/encryption_detection_bitlocker_test.vhd" skipexisting="true"/>
<get src="https://drive.google.com/uc?id=17sGybvmBGsWWJYo1IWKmO04oG9hKpPi3" dest="${test-input}/encryption_detection_sqlcipher_test.vhd" skipexisting="true"/>
<get src="https://drive.google.com/uc?id=0BxdBkzm5VKGNT0dGY0dqcHVsU3M" dest="${test-input}/filter_test1.img" skipexisting="true"/>
<get src="https://drive.google.com/uc?id=1bghoSm7z7nhmGIxlllyY1MMlbLntxm7n" dest="${test-input}/local_files_test.zip" skipexisting="true"/>
<get src="https://drive.google.com/uc?id=1BrSiUQ1fzxFS9vIaK4mYKX6qIVp9kRWT" dest="${test-input}/password_detection_test.img" skipexisting="true"/>
<get src="https://drive.google.com/uc?id=1HD8s4rculgHV1qZT5g80Kg7j4m1qccrN" dest="${test-input}/veracrypt_detection_test.vhd" skipexisting="true"/>
<get src="https://drive.google.com/uc?id=1O5D09fFCFpXZqw0uLEs8kVLtfYTxqXAd" dest="${test-input}/commonfiles_image1_v1.vhd" skipexisting="true"/>
<get src="https://drive.google.com/uc?id=1rMP1QTI0LdppzdypbG-4BDwkKcR3tHXc" dest="${test-input}/commonfiles_image2_v1.vhd" skipexisting="true"/>
<get src="https://drive.google.com/uc?id=1OdwyJ2lru55ZPdvwzj3pq6sXIys27i4x" dest="${test-input}/commonfiles_image3_v1.vhd" skipexisting="true"/>
<get src="https://drive.google.com/uc?id=1GoF2x0km5AyFvE926ttN20lrMX1oLN7E" dest="${test-input}/commonfiles_image4_v1.vhd" skipexisting="true"/>
</target>
<target name="get-deps" depends="init-ivy,getTSKJars,get-thirdparty-dependencies,get-InternalPythonModules, download-binlist,getTestDataFiles">
<mkdir dir="${ext.dir}"/>
<copy file="${thirdparty.dir}/LICENSE-2.0.txt" todir="${ext.dir}" />
<copy file="${thirdparty.dir}/LICENSE-2.0.txt" todir="${ext.dir}" />
<!-- fetch all the dependencies from Ivy and stick them in the right places -->
<ivy:resolve log="quiet"/>
<ivy:retrieve conf="core" pattern="${ext.dir}/[artifact]-[revision](-[classifier]).[ext]" />
</target>
</target>
<target name="init" depends="get-deps,harness.init"/>
@@ -1,70 +0,0 @@
/*
* Central Repository
*
* Copyright 2015-2017 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.centralrepository.contentviewer;
import java.util.Objects;
/**
* Used as a key to ensure we eliminate duplicates from the result set by not overwriting CR correlation instances.
*/
final class ArtifactKey {
private final String dataSourceID;
private final String filePath;
ArtifactKey(String theDataSource, String theFilePath) {
dataSourceID = theDataSource;
filePath = theFilePath.toLowerCase();
}
/**
*
* @return the dataSourceID device ID
*/
String getDataSourceID() {
return dataSourceID;
}
/**
*
* @return the filPath including the filename and extension.
*/
String getFilePath() {
return filePath;
}
@Override
public boolean equals(Object other) {
if (other instanceof ArtifactKey) {
return ((ArtifactKey) other).getDataSourceID().equals(dataSourceID) && ((ArtifactKey) other).getFilePath().equals(filePath);
}
return false;
}
@Override
public int hashCode() {
//int hash = 7;
//hash = 67 * hash + this.dataSourceID.hashCode();
//hash = 67 * hash + this.filePath.hashCode();
return Objects.hash(dataSourceID, filePath);
}
}
@@ -18,7 +18,9 @@
*/
package org.sleuthkit.autopsy.centralrepository.contentviewer;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedWriter;
@@ -31,19 +33,30 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Level;
import org.sleuthkit.autopsy.coreutils.Logger;
import java.util.stream.Collectors;
import javax.swing.GroupLayout;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import static javax.swing.JOptionPane.DEFAULT_OPTION;
import static javax.swing.JOptionPane.PLAIN_MESSAGE;
import static javax.swing.JOptionPane.ERROR_MESSAGE;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.LayoutStyle;
import javax.swing.ListSelectionModel;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import org.openide.awt.Mnemonics;
import org.openide.nodes.Node;
import org.openide.util.NbBundle;
import org.openide.util.NbBundle.Messages;
import org.openide.util.lookup.ServiceProvider;
import org.sleuthkit.autopsy.casemodule.Case;
@@ -65,15 +78,17 @@ import org.sleuthkit.datamodel.TskException;
import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskData;
import org.sleuthkit.datamodel.TskDataException;
/**
* View correlation results from other cases
*/
@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives
@ServiceProvider(service = DataContentViewer.class, position = 8)
@Messages({"DataContentViewerOtherCases.title=Other Occurrences",
"DataContentViewerOtherCases.toolTip=Displays instances of the selected file/artifact from other occurrences.",})
public class DataContentViewerOtherCases extends javax.swing.JPanel implements DataContentViewer {
public class DataContentViewerOtherCases extends JPanel implements DataContentViewer {
private final static Logger LOGGER = Logger.getLogger(DataContentViewerOtherCases.class.getName());
private final DataContentViewerOtherCasesTableModel tableModel;
@@ -401,7 +416,7 @@ public class DataContentViewerOtherCases extends javax.swing.JPanel implements D
// correlate on blackboard artifact attributes if they exist and supported
BlackboardArtifact bbArtifact = getBlackboardArtifactFromNode(node);
if (bbArtifact != null) {
if (bbArtifact != null && EamDb.isEnabled()) {
ret.addAll(EamArtifactUtil.getCorrelationAttributeFromBlackboardArtifact(bbArtifact, false, false));
}
@@ -451,12 +466,12 @@ public class DataContentViewerOtherCases extends javax.swing.JPanel implements D
*
* @return A collection of correlated artifact instances from other cases
*/
private Map<ArtifactKey, CorrelationAttributeInstance> getCorrelatedInstances(CorrelationAttribute corAttr, String dataSourceName, String deviceId) {
private Map<UniquePathKey,CorrelationAttributeInstance> getCorrelatedInstances(CorrelationAttribute corAttr, String dataSourceName, String deviceId) {
// @@@ Check exception
try {
final Case openCase = Case.getCurrentCase();
String caseUUID = openCase.getName();
HashMap<ArtifactKey, CorrelationAttributeInstance> artifactInstances = new HashMap<>();
HashMap<UniquePathKey,CorrelationAttributeInstance> artifactInstances = new HashMap<>();
if (EamDb.isEnabled()) {
EamDb dbManager = EamDb.getInstance();
@@ -464,8 +479,7 @@ public class DataContentViewerOtherCases extends javax.swing.JPanel implements D
.filter(artifactInstance -> !artifactInstance.getCorrelationCase().getCaseUUID().equals(caseUUID)
|| !artifactInstance.getCorrelationDataSource().getName().equals(dataSourceName)
|| !artifactInstance.getCorrelationDataSource().getDeviceID().equals(deviceId))
.collect(Collectors.toMap(
correlationAttr -> new ArtifactKey(correlationAttr.getCorrelationDataSource().getDeviceID(), correlationAttr.getFilePath()),
.collect(Collectors.toMap(correlationAttr -> new UniquePathKey(correlationAttr.getCorrelationDataSource().getDeviceID(), correlationAttr.getFilePath()),
correlationAttr -> correlationAttr)));
}
@@ -507,29 +521,59 @@ public class DataContentViewerOtherCases extends javax.swing.JPanel implements D
}
private void addOrUpdateAttributeInstance(final Case openCase, Map<ArtifactKey, CorrelationAttributeInstance> artifactInstances, AbstractFile caseDbFile) throws TskCoreException, EamDbException {
CorrelationCase caze = new CorrelationCase(openCase.getNumber(), openCase.getDisplayName());
CorrelationDataSource dataSource = CorrelationDataSource.fromTSKDataSource(caze, caseDbFile.getDataSource());
String filePath = caseDbFile.getParentPath() + caseDbFile.getName();
ArtifactKey instKey = new ArtifactKey(dataSource.getDeviceID(), filePath);
CorrelationAttributeInstance caseDbInstance = new CorrelationAttributeInstance(caze, dataSource, filePath, "", caseDbFile.getKnown());
TskData.FileKnown knownStatus = caseDbInstance.getKnownStatus();
// If not known, check Tags for known and set
TskData.FileKnown knownBad = TskData.FileKnown.BAD;
if (!knownStatus.equals(knownBad)) {
List<ContentTag> fileMatchTags = openCase.getServices().getTagsManager().getContentTagsByContent(caseDbFile);
/**
* Adds the file to the artifactInstances map if it does not already exist
*
* @param autopsyCase
* @param artifactInstances
* @param newFile
* @throws TskCoreException
* @throws EamDbException
*/
private void addOrUpdateAttributeInstance(final Case autopsyCase, Map<UniquePathKey,CorrelationAttributeInstance> artifactInstances, AbstractFile newFile) throws TskCoreException, EamDbException {
// figure out if the casedb file is known via either hash or tags
TskData.FileKnown localKnown = newFile.getKnown();
if (localKnown != TskData.FileKnown.BAD) {
List<ContentTag> fileMatchTags = autopsyCase.getServices().getTagsManager().getContentTagsByContent(newFile);
for (ContentTag tag : fileMatchTags) {
TskData.FileKnown tagKnownStatus = tag.getName().getKnownStatus();
if (tagKnownStatus.equals(knownBad)) {
caseDbInstance.setKnownStatus(knownBad);
if (tagKnownStatus.equals(TskData.FileKnown.BAD)) {
localKnown = TskData.FileKnown.BAD;
break;
}
}
}
// If known, or not in CR, add
if (caseDbInstance.getKnownStatus().equals(knownBad) || !artifactInstances.containsKey(instKey)) {
artifactInstances.put(instKey, caseDbInstance);
// make a key to see if the file is already in the map
String filePath = newFile.getParentPath() + newFile.getName();
String deviceId;
try {
deviceId = autopsyCase.getSleuthkitCase().getDataSource(newFile.getDataSource().getId()).getDeviceId();
} catch (TskDataException | TskCoreException ex) {
LOGGER.log(Level.WARNING, "Error getting data source info: " + ex);
return;
}
UniquePathKey uniquePathKey = new UniquePathKey(deviceId, filePath);
// double check that the CR version is BAD if the caseDB version is BAD.
if (artifactInstances.containsKey(uniquePathKey)) {
if (localKnown == TskData.FileKnown.BAD) {
CorrelationAttributeInstance prevInstance = artifactInstances.get(uniquePathKey);
prevInstance.setKnownStatus(localKnown);
}
}
// add the data from the case DB by pushing data into CorrelationAttributeInstance class
else {
// NOTE: If we are in here, it is likely because CR is not enabled. So, we cannot rely
// on any of the methods that query the DB.
CorrelationCase correlationCase = new CorrelationCase(autopsyCase.getName(), autopsyCase.getDisplayName());
CorrelationDataSource correlationDataSource = CorrelationDataSource.fromTSKDataSource(correlationCase, newFile.getDataSource());
CorrelationAttributeInstance caseDbInstance = new CorrelationAttributeInstance(correlationCase, correlationDataSource, filePath, "", localKnown);
artifactInstances.put(uniquePathKey, caseDbInstance);
}
}
@@ -539,9 +583,15 @@ public class DataContentViewerOtherCases extends javax.swing.JPanel implements D
// Is supported if this node
// has correlatable content (File, BlackboardArtifact) OR
// other common files across datasources.
return this.file != null
if(EamDb.isEnabled()){
return this.file != null
&& this.file.getSize() > 0
&& !getCorrelationAttributesFromNode(node).isEmpty();
} else{
return this.file != null
&& this.file.getSize() > 0;
}
}
@Override
@@ -582,7 +632,7 @@ public class DataContentViewerOtherCases extends javax.swing.JPanel implements D
// get the attributes we can correlate on
correlationAttributes.addAll(getCorrelationAttributesFromNode(node));
for (CorrelationAttribute corAttr : correlationAttributes) {
Map<ArtifactKey, CorrelationAttributeInstance> corAttrInstances = new HashMap<>(0);
Map<UniquePathKey, CorrelationAttributeInstance> corAttrInstances = new HashMap<>(0);
// get correlation and reference set instances from DB
corAttrInstances.putAll(getCorrelatedInstances(corAttr, dataSourceName, deviceId));
@@ -602,6 +652,7 @@ public class DataContentViewerOtherCases extends javax.swing.JPanel implements D
}
if (correlationAttributes.isEmpty()) {
// @@@ BC: We should have a more descriptive message than this. Mention that the file didn't have a MD5, etc.
displayMessageOnTableStatusPanel(Bundle.DataContentViewerOtherCases_table_noArtifacts());
} else if (0 == tableModel.getRowCount()) {
displayMessageOnTableStatusPanel(Bundle.DataContentViewerOtherCases_table_isempty());
@@ -639,131 +690,170 @@ public class DataContentViewerOtherCases extends javax.swing.JPanel implements D
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
rightClickPopupMenu = new javax.swing.JPopupMenu();
selectAllMenuItem = new javax.swing.JMenuItem();
exportToCSVMenuItem = new javax.swing.JMenuItem();
showCaseDetailsMenuItem = new javax.swing.JMenuItem();
showCommonalityMenuItem = new javax.swing.JMenuItem();
CSVFileChooser = new javax.swing.JFileChooser();
otherCasesPanel = new javax.swing.JPanel();
tableContainerPanel = new javax.swing.JPanel();
tableScrollPane = new javax.swing.JScrollPane();
otherCasesTable = new javax.swing.JTable();
tableStatusPanel = new javax.swing.JPanel();
tableStatusPanelLabel = new javax.swing.JLabel();
rightClickPopupMenu = new JPopupMenu();
selectAllMenuItem = new JMenuItem();
exportToCSVMenuItem = new JMenuItem();
showCaseDetailsMenuItem = new JMenuItem();
showCommonalityMenuItem = new JMenuItem();
CSVFileChooser = new JFileChooser();
otherCasesPanel = new JPanel();
tableContainerPanel = new JPanel();
tableScrollPane = new JScrollPane();
otherCasesTable = new JTable();
tableStatusPanel = new JPanel();
tableStatusPanelLabel = new JLabel();
org.openide.awt.Mnemonics.setLocalizedText(selectAllMenuItem, org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.selectAllMenuItem.text")); // NOI18N
Mnemonics.setLocalizedText(selectAllMenuItem, NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.selectAllMenuItem.text")); // NOI18N
rightClickPopupMenu.add(selectAllMenuItem);
org.openide.awt.Mnemonics.setLocalizedText(exportToCSVMenuItem, org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.exportToCSVMenuItem.text")); // NOI18N
Mnemonics.setLocalizedText(exportToCSVMenuItem, NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.exportToCSVMenuItem.text")); // NOI18N
rightClickPopupMenu.add(exportToCSVMenuItem);
org.openide.awt.Mnemonics.setLocalizedText(showCaseDetailsMenuItem, org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.showCaseDetailsMenuItem.text")); // NOI18N
Mnemonics.setLocalizedText(showCaseDetailsMenuItem, NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.showCaseDetailsMenuItem.text")); // NOI18N
rightClickPopupMenu.add(showCaseDetailsMenuItem);
org.openide.awt.Mnemonics.setLocalizedText(showCommonalityMenuItem, org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.showCommonalityMenuItem.text")); // NOI18N
Mnemonics.setLocalizedText(showCommonalityMenuItem, NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.showCommonalityMenuItem.text")); // NOI18N
rightClickPopupMenu.add(showCommonalityMenuItem);
setMinimumSize(new java.awt.Dimension(1500, 10));
setMinimumSize(new Dimension(1500, 10));
setOpaque(false);
setPreferredSize(new java.awt.Dimension(1500, 44));
setPreferredSize(new Dimension(1500, 44));
otherCasesPanel.setPreferredSize(new java.awt.Dimension(1500, 144));
otherCasesPanel.setPreferredSize(new Dimension(1500, 144));
tableContainerPanel.setPreferredSize(new java.awt.Dimension(1500, 63));
tableContainerPanel.setPreferredSize(new Dimension(1500, 63));
tableScrollPane.setPreferredSize(new java.awt.Dimension(1500, 30));
tableScrollPane.setPreferredSize(new Dimension(1500, 30));
otherCasesTable.setAutoCreateRowSorter(true);
otherCasesTable.setModel(tableModel);
otherCasesTable.setToolTipText(org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.table.toolTip.text")); // NOI18N
otherCasesTable.setToolTipText(NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.table.toolTip.text")); // NOI18N
otherCasesTable.setComponentPopupMenu(rightClickPopupMenu);
otherCasesTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_INTERVAL_SELECTION);
otherCasesTable.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
tableScrollPane.setViewportView(otherCasesTable);
tableStatusPanel.setPreferredSize(new java.awt.Dimension(1500, 16));
tableStatusPanel.setPreferredSize(new Dimension(1500, 16));
tableStatusPanelLabel.setForeground(new java.awt.Color(255, 0, 51));
tableStatusPanelLabel.setForeground(new Color(255, 0, 51));
javax.swing.GroupLayout tableStatusPanelLayout = new javax.swing.GroupLayout(tableStatusPanel);
GroupLayout tableStatusPanelLayout = new GroupLayout(tableStatusPanel);
tableStatusPanel.setLayout(tableStatusPanelLayout);
tableStatusPanelLayout.setHorizontalGroup(
tableStatusPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
tableStatusPanelLayout.setHorizontalGroup(tableStatusPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGap(0, 0, Short.MAX_VALUE)
.addGroup(tableStatusPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(tableStatusPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGroup(tableStatusPanelLayout.createSequentialGroup()
.addContainerGap()
.addComponent(tableStatusPanelLabel, javax.swing.GroupLayout.DEFAULT_SIZE, 780, Short.MAX_VALUE)
.addComponent(tableStatusPanelLabel, GroupLayout.DEFAULT_SIZE, 780, Short.MAX_VALUE)
.addContainerGap()))
);
tableStatusPanelLayout.setVerticalGroup(
tableStatusPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
tableStatusPanelLayout.setVerticalGroup(tableStatusPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGap(0, 16, Short.MAX_VALUE)
.addGroup(tableStatusPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(tableStatusPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGroup(tableStatusPanelLayout.createSequentialGroup()
.addComponent(tableStatusPanelLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(tableStatusPanelLabel, GroupLayout.PREFERRED_SIZE, 16, GroupLayout.PREFERRED_SIZE)
.addGap(0, 0, Short.MAX_VALUE)))
);
javax.swing.GroupLayout tableContainerPanelLayout = new javax.swing.GroupLayout(tableContainerPanel);
GroupLayout tableContainerPanelLayout = new GroupLayout(tableContainerPanel);
tableContainerPanel.setLayout(tableContainerPanelLayout);
tableContainerPanelLayout.setHorizontalGroup(
tableContainerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(tableScrollPane, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(tableStatusPanel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
tableContainerPanelLayout.setHorizontalGroup(tableContainerPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addComponent(tableScrollPane, GroupLayout.Alignment.TRAILING, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(tableStatusPanel, GroupLayout.Alignment.TRAILING, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
);
tableContainerPanelLayout.setVerticalGroup(
tableContainerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
tableContainerPanelLayout.setVerticalGroup(tableContainerPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGroup(tableContainerPanelLayout.createSequentialGroup()
.addComponent(tableScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(tableStatusPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(tableScrollPane, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
.addComponent(tableStatusPanel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addContainerGap())
);
javax.swing.GroupLayout otherCasesPanelLayout = new javax.swing.GroupLayout(otherCasesPanel);
GroupLayout otherCasesPanelLayout = new GroupLayout(otherCasesPanel);
otherCasesPanel.setLayout(otherCasesPanelLayout);
otherCasesPanelLayout.setHorizontalGroup(
otherCasesPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
otherCasesPanelLayout.setHorizontalGroup(otherCasesPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGap(0, 1500, Short.MAX_VALUE)
.addGroup(otherCasesPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(tableContainerPanel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
.addGroup(otherCasesPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addComponent(tableContainerPanel, GroupLayout.Alignment.TRAILING, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
);
otherCasesPanelLayout.setVerticalGroup(
otherCasesPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
otherCasesPanelLayout.setVerticalGroup(otherCasesPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGap(0, 60, Short.MAX_VALUE)
.addGroup(otherCasesPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(otherCasesPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGroup(otherCasesPanelLayout.createSequentialGroup()
.addComponent(tableContainerPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 60, Short.MAX_VALUE)
.addComponent(tableContainerPanel, GroupLayout.DEFAULT_SIZE, 60, Short.MAX_VALUE)
.addGap(0, 0, 0)))
);
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
GroupLayout layout = new GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(otherCasesPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
layout.setHorizontalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addComponent(otherCasesPanel, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(otherCasesPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 60, Short.MAX_VALUE)
layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addComponent(otherCasesPanel, GroupLayout.DEFAULT_SIZE, 60, Short.MAX_VALUE)
);
}// </editor-fold>//GEN-END:initComponents
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JFileChooser CSVFileChooser;
private javax.swing.JMenuItem exportToCSVMenuItem;
private javax.swing.JPanel otherCasesPanel;
private javax.swing.JTable otherCasesTable;
private javax.swing.JPopupMenu rightClickPopupMenu;
private javax.swing.JMenuItem selectAllMenuItem;
private javax.swing.JMenuItem showCaseDetailsMenuItem;
private javax.swing.JMenuItem showCommonalityMenuItem;
private javax.swing.JPanel tableContainerPanel;
private javax.swing.JScrollPane tableScrollPane;
private javax.swing.JPanel tableStatusPanel;
private javax.swing.JLabel tableStatusPanelLabel;
private JFileChooser CSVFileChooser;
private JMenuItem exportToCSVMenuItem;
private JPanel otherCasesPanel;
private JTable otherCasesTable;
private JPopupMenu rightClickPopupMenu;
private JMenuItem selectAllMenuItem;
private JMenuItem showCaseDetailsMenuItem;
private JMenuItem showCommonalityMenuItem;
private JPanel tableContainerPanel;
private JScrollPane tableScrollPane;
private JPanel tableStatusPanel;
private JLabel tableStatusPanelLabel;
// End of variables declaration//GEN-END:variables
/**
* Used as a key to ensure we eliminate duplicates from the result set by not overwriting CR correlation instances.
*/
static final class UniquePathKey {
private final String dataSourceID;
private final String filePath;
UniquePathKey(String theDataSource, String theFilePath) {
super();
dataSourceID = theDataSource;
filePath = theFilePath.toLowerCase();
}
/**
*
* @return the dataSourceID device ID
*/
String getDataSourceID() {
return dataSourceID;
}
/**
*
* @return the filPath including the filename and extension.
*/
String getFilePath() {
return filePath;
}
@Override
public boolean equals(Object other) {
if (other instanceof UniquePathKey) {
return ((UniquePathKey) other).getDataSourceID().equals(dataSourceID) && ((UniquePathKey) other).getFilePath().equals(filePath);
}
return false;
}
@Override
public int hashCode() {
//int hash = 7;
//hash = 67 * hash + this.dataSourceID.hashCode();
//hash = 67 * hash + this.filePath.hashCode();
return Objects.hash(dataSourceID, filePath);
}
}
}
@@ -39,6 +39,8 @@ import org.openide.util.NbBundle.Messages;
import org.sleuthkit.autopsy.casemodule.Case;
import static org.sleuthkit.autopsy.centralrepository.datamodel.EamDbUtil.updateSchemaVersion;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.healthmonitor.EnterpriseHealthMonitor;
import org.sleuthkit.autopsy.healthmonitor.TimingMetric;
import org.sleuthkit.datamodel.CaseDbSchemaVersionNumber;
import org.sleuthkit.datamodel.TskData;
@@ -938,6 +940,8 @@ public abstract class AbstractSqlEamDb implements EamDb {
if (bulkArtifactsCount == 0) {
return;
}
TimingMetric timingMetric = EnterpriseHealthMonitor.getTimingMetric("Correlation Engine: Bulk insert");
for (CorrelationAttribute.Type type : artifactTypes) {
@@ -988,6 +992,8 @@ public abstract class AbstractSqlEamDb implements EamDb {
bulkPs.executeBatch();
bulkArtifacts.get(type.getDbTableName()).clear();
}
EnterpriseHealthMonitor.submitTimingMetric(timingMetric);
// Reset state
bulkArtifactsCount = 0;
@@ -226,13 +226,10 @@ public class EamArtifactUtil {
* Does not add the artifact to the database.
*
* @param content The content object
* @param knownStatus Unknown, notable, or known
* @param comment The comment for the new artifact (generally used for a
* tag comment)
*
* @return The new EamArtifact or null if creation failed
*/
public static CorrelationAttribute getCorrelationAttributeFromContent(Content content, TskData.FileKnown knownStatus, String comment) {
public static CorrelationAttribute makeCorrelationAttributeFromContent(Content content) {
if (!(content instanceof AbstractFile)) {
return null;
@@ -261,10 +258,7 @@ public class EamArtifactUtil {
CorrelationAttributeInstance cei = new CorrelationAttributeInstance(
correlationCase,
CorrelationDataSource.fromTSKDataSource(correlationCase, af.getDataSource()),
af.getParentPath() + af.getName(),
comment,
knownStatus
);
af.getParentPath() + af.getName());
eamArtifact.addInstance(cei);
return eamArtifact;
} catch (TskCoreException | EamDbException | NoCurrentCaseException ex) {
@@ -192,8 +192,7 @@ final class CaseEventListener implements PropertyChangeListener {
}
}
final CorrelationAttribute eamArtifact = EamArtifactUtil.getCorrelationAttributeFromContent(af,
knownStatus, comment);
final CorrelationAttribute eamArtifact = EamArtifactUtil.makeCorrelationAttributeFromContent(af);
if (eamArtifact != null) {
// send update to Central Repository db
@@ -402,8 +401,7 @@ final class CaseEventListener implements PropertyChangeListener {
}
//if the file will have no tags with a status which would prevent the current status from being changed
if (!hasTagWithConflictingKnownStatus) {
final CorrelationAttribute eamArtifact = EamArtifactUtil.getCorrelationAttributeFromContent(contentTag.getContent(),
tagName.getKnownStatus(), "");
final CorrelationAttribute eamArtifact = EamArtifactUtil.makeCorrelationAttributeFromContent(contentTag.getContent());
if (eamArtifact != null) {
EamDb.getInstance().setArtifactInstanceKnownStatus(eamArtifact, tagName.getKnownStatus());
}
@@ -1,61 +0,0 @@
/*
* Central Repository
*
* Copyright 2015-2017 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.centralrepository.eventlisteners;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Level;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttribute;
import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb;
import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException;
/**
* Thread to insert a new artifact into remote DB.
*/
public class NewArtifactsRunner implements Runnable {
private static final Logger LOGGER = Logger.getLogger(NewArtifactsRunner.class.getName());
private static final long serialVersionUID = 1L;
private final Collection<CorrelationAttribute> eamArtifacts;
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public NewArtifactsRunner(Collection<CorrelationAttribute> eamArtifacts) {
this.eamArtifacts = new ArrayList(eamArtifacts);
}
@Override
public void run() {
if (!EamDb.isEnabled()) {
LOGGER.log(Level.WARNING, "Central Repository database not configured"); // NON-NLS
return;
}
try {
EamDb dbManager = EamDb.getInstance();
for (CorrelationAttribute eamArtifact : eamArtifacts) {
dbManager.addArtifact(eamArtifact);
}
} catch (EamDbException ex) {
LOGGER.log(Level.SEVERE, "Error connecting to Central Repository database.", ex); //NON-NLS
}
}
}
@@ -49,6 +49,8 @@ import org.sleuthkit.datamodel.HashUtility;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData;
import org.sleuthkit.autopsy.centralrepository.eventlisteners.IngestEventsListener;
import org.sleuthkit.autopsy.healthmonitor.EnterpriseHealthMonitor;
import org.sleuthkit.autopsy.healthmonitor.TimingMetric;
/**
* Ingest module for inserting entries into the Central Repository database on
@@ -129,7 +131,9 @@ final class IngestModule implements FileIngestModule {
*/
if (abstractFile.getKnown() != TskData.FileKnown.KNOWN && flagTaggedNotableItems) {
try {
TimingMetric timingMetric = EnterpriseHealthMonitor.getTimingMetric("Correlation Engine: Notable artifact query");
List<String> caseDisplayNamesList = dbManager.getListCasesHavingArtifactInstancesKnownBad(filesType, md5);
EnterpriseHealthMonitor.submitTimingMetric(timingMetric);
if (!caseDisplayNamesList.isEmpty()) {
postCorrelatedBadFileToBlackboard(abstractFile, caseDisplayNamesList);
}
@@ -24,7 +24,7 @@ import java.util.Map;
/**
* Provides logic for selecting common files from all data sources.
*/
final class AllDataSourcesCommonFilesAlgorithm extends CommonFilesMetadataBuilder {
final public class AllDataSourcesCommonFilesAlgorithm extends CommonFilesMetadataBuilder {
private static final String WHERE_CLAUSE = "%s md5 in (select md5 from tsk_files where (known != 1 OR known IS NULL)%s GROUP BY md5 HAVING COUNT(*) > 1) order by md5"; //NON-NLS
@@ -36,7 +36,7 @@ final class AllDataSourcesCommonFilesAlgorithm extends CommonFilesMetadataBuilde
* @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
*/
AllDataSourcesCommonFilesAlgorithm(Map<Long, String> dataSourceIdMap, boolean filterByMediaMimeType, boolean filterByDocMimeType) {
public AllDataSourcesCommonFilesAlgorithm(Map<Long, String> dataSourceIdMap, boolean filterByMediaMimeType, boolean filterByDocMimeType) {
super(dataSourceIdMap, filterByMediaMimeType, filterByDocMimeType);
}
@@ -26,7 +26,7 @@ import java.util.Map;
* Utility and wrapper model around data required for Common Files Search results.
* Subclass this to implement different selections of files from the case.
*/
final class CommonFilesMetadata {
final public class CommonFilesMetadata {
private final Map<String, Md5Metadata> metadata;
@@ -52,7 +52,7 @@ final class CommonFilesMetadata {
return this.metadata.get(md5);
}
Map<String, Md5Metadata> getMetadata() {
public Map<String, Md5Metadata> getMetadata() {
return Collections.unmodifiableMap(this.metadata);
}
@@ -60,7 +60,7 @@ final class CommonFilesMetadata {
* How many distinct file instances exist for this metadata?
* @return number of file instances
*/
int size() {
public int size() {
int count = 0;
for (Md5Metadata data : this.metadata.values()) {
count += data.size();
@@ -46,7 +46,7 @@ import org.sleuthkit.datamodel.TskCoreException;
* This entire thing runs on a background thread where exceptions are handled.
*/
@SuppressWarnings("PMD.AbstractNaming")
abstract class CommonFilesMetadataBuilder {
public abstract class CommonFilesMetadataBuilder {
private final Map<Long, String> dataSourceIdToNameMap;
private final boolean filterByMedia;
@@ -18,12 +18,9 @@
*/
package org.sleuthkit.autopsy.commonfilesearch;
import java.io.File;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;
@@ -33,7 +30,6 @@ import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import org.openide.explorer.ExplorerManager;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer;
import org.sleuthkit.autopsy.corecomponents.DataResultTopComponent;
@@ -42,9 +38,6 @@ import org.sleuthkit.autopsy.corecomponents.TableFilterNode;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.directorytree.DataResultFilterNode;
import org.sleuthkit.autopsy.directorytree.DirectoryTreeTopComponent;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.SleuthkitCase.CaseDbQuery;
import org.sleuthkit.datamodel.TskCoreException;
/**
@@ -77,7 +70,7 @@ public final class CommonFilesPanel extends javax.swing.JPanel {
initComponents();
this.setupDataSources();
this.errorText.setVisible(false);
}
@@ -98,10 +91,6 @@ public final class CommonFilesPanel extends javax.swing.JPanel {
new SwingWorker<Map<Long, String>, Void>() {
private static final String SELECT_DATA_SOURCES_LOGICAL = "select obj_id, name from tsk_files where obj_id in (SELECT obj_id FROM tsk_objects WHERE obj_id in (select obj_id from data_source_info))";
private static final String SELECT_DATA_SOURCES_IMAGE = "select obj_id, name from tsk_image_names where obj_id in (SELECT obj_id FROM tsk_objects WHERE obj_id in (select obj_id from data_source_info))";
private void updateUi() {
String[] dataSourcesNames = new String[CommonFilesPanel.this.dataSourceMap.size()];
@@ -132,48 +121,10 @@ public final class CommonFilesPanel extends javax.swing.JPanel {
return CommonFilesPanel.this.dataSourceMap.size() >= 2;
}
private void loadLogicalSources(SleuthkitCase tskDb, Map<Long, String> dataSouceMap) throws TskCoreException, SQLException {
//try block releases resources - exceptions are handled in done()
try (
CaseDbQuery query = tskDb.executeQuery(SELECT_DATA_SOURCES_LOGICAL);
ResultSet resultSet = query.getResultSet()) {
while (resultSet.next()) {
Long objectId = resultSet.getLong(1);
String dataSourceName = resultSet.getString(2);
dataSouceMap.put(objectId, dataSourceName);
}
}
}
private void loadImageSources(SleuthkitCase tskDb, Map<Long, String> dataSouceMap) throws SQLException, TskCoreException {
//try block releases resources - exceptions are handled in done()
try (
CaseDbQuery query = tskDb.executeQuery(SELECT_DATA_SOURCES_IMAGE);
ResultSet resultSet = query.getResultSet()) {
while (resultSet.next()) {
Long objectId = resultSet.getLong(1);
String dataSourceName = resultSet.getString(2);
File image = new File(dataSourceName);
String dataSourceNameTrimmed = image.getName();
dataSouceMap.put(objectId, dataSourceNameTrimmed);
}
}
}
@Override
protected Map<Long, String> doInBackground() throws NoCurrentCaseException, TskCoreException, SQLException {
Map<Long, String> dataSouceMap = new HashMap<>();
Case currentCase = Case.getCurrentCaseThrows();
SleuthkitCase tskDb = currentCase.getSleuthkitCase();
loadLogicalSources(tskDb, dataSouceMap);
loadImageSources(tskDb, dataSouceMap);
return dataSouceMap;
DataSourceLoader loader = new DataSourceLoader();
return loader.getDataSourceMap();
}
@Override
@@ -296,10 +247,10 @@ public final class CommonFilesPanel extends javax.swing.JPanel {
TableFilterNode tableFilterWithDescendantsNode = new TableFilterNode(dataResultFilterNode);
DataResultViewerTable table = new DataResultViewerTable();
Collection<DataResultViewer> viewers = new ArrayList<>(1);
viewers.add(table);
DataResultTopComponent.createInstance(tabTitle, pathText, tableFilterWithDescendantsNode, metadata.size(), viewers);
} catch (InterruptedException ex) {
@@ -591,7 +542,7 @@ public final class CommonFilesPanel extends javax.swing.JPanel {
this.pictureVideoCheckbox.setEnabled(true);
this.documentsCheckbox.setEnabled(true);
this.toggleErrorTextAndSearchBox();
}
}
@@ -0,0 +1,95 @@
/*
*
* Autopsy Forensic Browser
*
* Copyright 2018 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.commonfilesearch;
import java.io.File;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
/**
* Encapsulates logic required to create a mapping of data sources in the
* current case to their data source IDs.
*
* Intended to be used within the context of a SwingWorker or other background
* thread.
*/
public class DataSourceLoader {
private static final String SELECT_DATA_SOURCES_LOGICAL = "select obj_id, name from tsk_files where obj_id in (SELECT obj_id FROM tsk_objects WHERE obj_id in (select obj_id from data_source_info))";
private static final String SELECT_DATA_SOURCES_IMAGE = "select obj_id, name from tsk_image_names where obj_id in (SELECT obj_id FROM tsk_objects WHERE obj_id in (select obj_id from data_source_info))";
private void loadLogicalSources(SleuthkitCase tskDb, Map<Long, String> dataSouceMap) throws TskCoreException, SQLException {
//try block releases resources - exceptions are handled in done()
try (
SleuthkitCase.CaseDbQuery query = tskDb.executeQuery(SELECT_DATA_SOURCES_LOGICAL);
ResultSet resultSet = query.getResultSet()
) {
while (resultSet.next()) {
Long objectId = resultSet.getLong(1);
String dataSourceName = resultSet.getString(2);
dataSouceMap.put(objectId, dataSourceName);
}
}
}
private void loadImageSources(SleuthkitCase tskDb, Map<Long, String> dataSouceMap) throws SQLException, TskCoreException {
//try block releases resources - exceptions are handled in done()
try (
SleuthkitCase.CaseDbQuery query = tskDb.executeQuery(SELECT_DATA_SOURCES_IMAGE);
ResultSet resultSet = query.getResultSet()) {
while (resultSet.next()) {
Long objectId = resultSet.getLong(1);
String dataSourceName = resultSet.getString(2);
File image = new File(dataSourceName);
String dataSourceNameTrimmed = image.getName();
dataSouceMap.put(objectId, dataSourceNameTrimmed);
}
}
}
/**
* Get a map of data source Ids to their string names for the current case.
*
* @return Map of Long (id) to String (name)
* @throws NoCurrentCaseException
* @throws TskCoreException
* @throws SQLException
*/
public Map<Long, String> getDataSourceMap() throws NoCurrentCaseException, TskCoreException, SQLException {
Map<Long, String> dataSouceMap = new HashMap<>();
Case currentCase = Case.getCurrentCaseThrows();
SleuthkitCase tskDb = currentCase.getSleuthkitCase();
loadLogicalSources(tskDb, dataSouceMap);
loadImageSources(tskDb, dataSouceMap);
return dataSouceMap;
}
}
@@ -24,7 +24,7 @@ import java.util.Map;
/**
* Provides logic for selecting common files from a single data source.
*/
final class SingleDataSource extends CommonFilesMetadataBuilder {
final public class SingleDataSource extends CommonFilesMetadataBuilder {
private static final String WHERE_CLAUSE = "%s md5 in (select md5 from tsk_files where md5 in (select md5 from tsk_files where (known != 1 OR known IS NULL) and data_source_obj_id=%s%s) GROUP BY md5 HAVING COUNT(*) > 1) order by md5"; //NON-NLS
private final Long selectedDataSourceId;
@@ -35,10 +35,12 @@ final class SingleDataSource extends CommonFilesMetadataBuilder {
* 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 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 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
*/
SingleDataSource(Long dataSourceId, Map<Long, String> dataSourceIdMap, boolean filterByMediaMimeType, boolean filterByDocMimeType) {
public SingleDataSource(Long dataSourceId, Map<Long, String> dataSourceIdMap, boolean filterByMediaMimeType, boolean filterByDocMimeType) {
super(dataSourceIdMap, filterByMediaMimeType, filterByDocMimeType);
this.selectedDataSourceId = dataSourceId;
this.dataSourceName = dataSourceIdMap.get(this.selectedDataSourceId);
@@ -253,7 +253,7 @@ final public class FiltersPanel extends JPanel {
});
}
} catch (NoCurrentCaseException ex) {
logger.log(Level.WARNING, "Communications Visualization Tool opened with no open case.", ex);
logger.log(Level.INFO, "Filter update cancelled. Case is closed.");
} catch (TskCoreException tskCoreException) {
logger.log(Level.SEVERE, "There was a error loading the datasources for the case.", tskCoreException);
}
@@ -63,7 +63,7 @@ public interface DataResult {
* Sets the descriptive text about the source of the nodes displayed in this
* result view component.
*
* @param description The text to display.
* @param pathText The text to display.
*/
public void setPath(String pathText);
@@ -734,7 +734,7 @@ public class DataResultPanel extends javax.swing.JPanel implements DataResult, C
*
* @return True or false.
*
* @Deprecated This method has no valid use case.
* @deprecated This method has no valid use case.
*/
@Deprecated
@Override
@@ -28,7 +28,7 @@ public interface AutopsyVisitableItem {
/**
* visitor pattern support
*
* @param v visitor
* @param visitor visitor
*
* @return visitor return value
*/
@@ -40,7 +40,7 @@ abstract class ContentNode extends DisplayableItemNode {
/**
* Visitor pattern support.
*
* @param v visitor
* @param visitor visitor
*
* @return visitor's visit return value
*/
@@ -28,8 +28,11 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map;
import java.util.List;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -37,6 +40,7 @@ import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.Random;
import org.apache.commons.dbcp2.BasicDataSource;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
@@ -44,7 +48,6 @@ import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
import org.sleuthkit.autopsy.core.UserPreferences;
import org.sleuthkit.autopsy.core.UserPreferencesException;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ModuleSettings;
import org.sleuthkit.autopsy.coreutils.ThreadUtils;
import org.sleuthkit.datamodel.CaseDbConnectionInfo;
import org.sleuthkit.datamodel.CaseDbSchemaVersionNumber;
@@ -64,8 +67,6 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
private final static Logger logger = Logger.getLogger(EnterpriseHealthMonitor.class.getName());
private final static String DATABASE_NAME = "EnterpriseHealthMonitor";
private final static String MODULE_NAME = "EnterpriseHealthMonitor";
private final static String IS_ENABLED_KEY = "is_enabled";
private final static long DATABASE_WRITE_INTERVAL = 60; // Minutes
public static final CaseDbSchemaVersionNumber CURRENT_DB_SCHEMA_VERSION
= new CaseDbSchemaVersionNumber(1, 0);
@@ -80,6 +81,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
private final Map<String, TimingInfo> timingInfoMap;
private static final int CONN_POOL_SIZE = 10;
private BasicDataSource connectionPool = null;
private CaseDbConnectionInfo connectionSettingsInUse = null;
private String hostName;
private EnterpriseHealthMonitor() throws HealthMonitorException {
@@ -100,22 +102,11 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
logger.log(Level.SEVERE, "Unable to look up host name - falling back to UUID " + hostName, ex);
}
// Read from module settings to determine if the module is enabled
if (ModuleSettings.settingExists(MODULE_NAME, IS_ENABLED_KEY)) {
if(ModuleSettings.getConfigSetting(MODULE_NAME, IS_ENABLED_KEY).equals("true")){
isEnabled.set(true);
try {
activateMonitor();
} catch (HealthMonitorException ex) {
// If we failed to activate it, then disable the monitor
logger.log(Level.SEVERE, "Health monitor activation failed - disabling health monitor");
setEnabled(false);
throw ex;
}
return;
}
}
isEnabled.set(false);
// Read from the database to determine if the module is enabled
updateFromGlobalEnabledStatus();
// Start the timer for database checks and writes
startTimer();
}
/**
@@ -137,10 +128,13 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
* out of the maps, and sets up the timer for writing to the database.
* @throws HealthMonitorException
*/
private synchronized void activateMonitor() throws HealthMonitorException {
private synchronized void activateMonitorLocally() throws HealthMonitorException {
logger.log(Level.INFO, "Activating Servies Health Monitor");
// Make sure there are no left over connections to an old database
shutdownConnections();
if (!UserPreferences.getIsMultiUserModeEnabled()) {
throw new HealthMonitorException("Multi user mode is not enabled - can not activate health monitor");
}
@@ -168,9 +162,6 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
// Clear out any old data
timingInfoMap.clear();
// Start the timer for database writes
startTimer();
}
/**
@@ -180,16 +171,13 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
* and shuts down the connection pool.
* @throws HealthMonitorException
*/
private synchronized void deactivateMonitor() throws HealthMonitorException {
private synchronized void deactivateMonitorLocally() throws HealthMonitorException {
logger.log(Level.INFO, "Deactivating Servies Health Monitor");
// Clear out the collected data
timingInfoMap.clear();
// Stop the timer
stopTimer();
// Shut down the connection pool
shutdownConnections();
}
@@ -202,7 +190,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
stopTimer();
healthMonitorOutputTimer = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat("health_monitor_timer").build());
healthMonitorOutputTimer.scheduleWithFixedDelay(new DatabaseWriteTask(), DATABASE_WRITE_INTERVAL, DATABASE_WRITE_INTERVAL, TimeUnit.MINUTES);
healthMonitorOutputTimer.scheduleWithFixedDelay(new PeriodicHealthMonitorTask(), DATABASE_WRITE_INTERVAL, DATABASE_WRITE_INTERVAL, TimeUnit.MINUTES);
}
/**
@@ -234,15 +222,18 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
}
if(enabled) {
getInstance().activateMonitor();
getInstance().activateMonitorLocally();
// If activateMonitor fails, we won't update either of these
ModuleSettings.setConfigSetting(MODULE_NAME, IS_ENABLED_KEY, "true");
// If activateMonitor fails, we won't update this
getInstance().setGlobalEnabledStatusInDB(true);
isEnabled.set(true);
} else {
ModuleSettings.setConfigSetting(MODULE_NAME, IS_ENABLED_KEY, "false");
if(isEnabled.get()) {
// If we were enabled before, set the global state to disabled
getInstance().setGlobalEnabledStatusInDB(false);
}
isEnabled.set(false);
getInstance().deactivateMonitor();
getInstance().deactivateMonitorLocally();
}
}
@@ -460,7 +451,6 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
String createCommand = "SELECT 1 AS result FROM pg_database WHERE datname='" + DATABASE_NAME + "'";
rs = statement.executeQuery(createCommand);
if(rs.next()) {
logger.log(Level.INFO, "Existing Enterprise Health Monitor database found");
return true;
}
} finally {
@@ -501,6 +491,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
private void setupConnectionPool() throws HealthMonitorException {
try {
CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo();
connectionSettingsInUse = db;
connectionPool = new BasicDataSource();
connectionPool.setDriverClassName("org.postgresql.Driver");
@@ -598,6 +589,108 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
}
}
/**
* Return whether the health monitor is locally enabled.
* This does not query the database.
* @return true if it is enabled, false otherwise
*/
static boolean monitorIsEnabled() {
return isEnabled.get();
}
/**
* Check whether monitoring should be enabled from the monitor database
* and enable/disable as needed.
* @throws HealthMonitorException
*/
synchronized void updateFromGlobalEnabledStatus() throws HealthMonitorException {
boolean previouslyEnabled = monitorIsEnabled();
// We can't even check the database if multi user settings aren't enabled.
if (!UserPreferences.getIsMultiUserModeEnabled()) {
isEnabled.set(false);
if(previouslyEnabled) {
deactivateMonitorLocally();
}
return;
}
// If the health monitor database doesn't exist or if it is not initialized,
// then monitoring isn't enabled
if ((! databaseExists()) || (! databaseIsInitialized())) {
isEnabled.set(false);
if(previouslyEnabled) {
deactivateMonitorLocally();
}
return;
}
// If we're currently enabled, check whether the multiuser settings have changed.
// If they have, force a reset on the connection pool.
if(previouslyEnabled && (connectionSettingsInUse != null)) {
try {
CaseDbConnectionInfo currentSettings = UserPreferences.getDatabaseConnectionInfo();
if(! (connectionSettingsInUse.getUserName().equals(currentSettings.getUserName())
&& connectionSettingsInUse.getPassword().equals(currentSettings.getPassword())
&& connectionSettingsInUse.getPort().equals(currentSettings.getPort())
&& connectionSettingsInUse.getHost().equals(currentSettings.getHost()) )) {
shutdownConnections();
}
} catch (UserPreferencesException ex) {
throw new HealthMonitorException("Error reading database connection info", ex);
}
}
boolean currentlyEnabled = getGlobalEnabledStatusFromDB();
if( currentlyEnabled != previouslyEnabled) {
if( ! currentlyEnabled ) {
isEnabled.set(false);
deactivateMonitorLocally();
} else {
isEnabled.set(true);
activateMonitorLocally();
}
}
}
/**
* Read the enabled status from the database.
* Check that the health monitor database exists before calling this.
* @return true if the database is enabled, false otherwise
* @throws HealthMonitorException
*/
private boolean getGlobalEnabledStatusFromDB() throws HealthMonitorException {
try (Connection conn = connect();
Statement statement = conn.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='MONITOR_ENABLED'")) {
if (resultSet.next()) {
return(resultSet.getBoolean("value"));
}
throw new HealthMonitorException("No enabled status found in database");
} catch (SQLException ex) {
throw new HealthMonitorException("Error initializing database", ex);
}
}
/**
* Set the global enabled status in the database.
* @throws HealthMonitorException
*/
private void setGlobalEnabledStatusInDB(boolean status) throws HealthMonitorException {
try (Connection conn = connect();
Statement statement = conn.createStatement();) {
statement.execute("UPDATE db_info SET value='" + status + "' WHERE name='MONITOR_ENABLED'");
} catch (SQLException ex) {
throw new HealthMonitorException("Error setting enabled status", ex);
}
}
/**
* Get the current schema version
* @return the current schema version
@@ -611,7 +704,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
ResultSet resultSet = null;
try (Statement statement = conn.createStatement()) {
int minorVersion = 0;
int minorVersion = 0;
int majorVersion = 0;
resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='SCHEMA_MINOR_VERSION'");
if (resultSet.next()) {
@@ -688,6 +781,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
statement.execute("INSERT INTO db_info (name, value) VALUES ('SCHEMA_VERSION', '" + CURRENT_DB_SCHEMA_VERSION.getMajor() + "')");
statement.execute("INSERT INTO db_info (name, value) VALUES ('SCHEMA_MINOR_VERSION', '" + CURRENT_DB_SCHEMA_VERSION.getMinor() + "')");
statement.execute("INSERT INTO db_info (name, value) VALUES ('MONITOR_ENABLED', 'true')");
conn.commit();
} catch (SQLException ex) {
@@ -708,20 +802,24 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
/**
* The task called by the ScheduledThreadPoolExecutor to handle
* the database writes.
* the database checks/writes.
*/
static final class DatabaseWriteTask implements Runnable {
static final class PeriodicHealthMonitorTask implements Runnable {
/**
* Write current metric data to the database
* Perform all periodic tasks:
* - Check if monitoring has been enabled / disabled in the database
* - Gather any additional metrics
* - Write current metric data to the database
*/
@Override
public void run() {
try {
getInstance().updateFromGlobalEnabledStatus();
getInstance().gatherTimerBasedMetrics();
getInstance().writeCurrentStateToDatabase();
} catch (HealthMonitorException ex) {
logger.log(Level.SEVERE, "Error writing current metrics to database", ex); //NON-NLS
logger.log(Level.SEVERE, "Error performing periodic task", ex); //NON-NLS
}
}
}
@@ -734,12 +832,220 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
case CURRENT_CASE:
if ((null == evt.getNewValue()) && (evt.getOldValue() instanceof Case)) {
// When a case is closed, write the current metrics to the database
healthMonitorExecutor.submit(new EnterpriseHealthMonitor.DatabaseWriteTask());
healthMonitorExecutor.submit(new EnterpriseHealthMonitor.PeriodicHealthMonitorTask());
}
break;
}
}
/**
* Debugging method to generate sample data for the database.
* It will delete all current timing data and replace it with randomly generated values.
* If there is more than one node, the second node's times will trend upwards.
*/
void populateDatabaseWithSampleData(int nDays, int nNodes, boolean createVerificationData) throws HealthMonitorException {
if(! isEnabled.get()) {
throw new HealthMonitorException("Can't populate database - monitor not enabled");
}
// Get the database lock
CoordinationService.Lock lock = getSharedDbLock();
if(lock == null) {
throw new HealthMonitorException("Error getting database lock");
}
String[] metricNames = {"Disk Reads: Hash calculation", "Database: getImages query", "Solr: Index chunk", "Solr: Connectivity check"}; // NON-NLS
Random rand = new Random();
long maxTimestamp = System.currentTimeMillis();
long millisPerHour = 1000 * 60 * 60;
long minTimestamp = maxTimestamp - (nDays * (millisPerHour * 24));
Connection conn = null;
try {
conn = connect();
if(conn == null) {
throw new HealthMonitorException("Error getting database connection");
}
try (Statement statement = conn.createStatement()) {
statement.execute("DELETE FROM timing_data"); // NON-NLS
} catch (SQLException ex) {
logger.log(Level.SEVERE, "Error clearing timing data", ex);
return;
}
// Add timing metrics to the database
String addTimingInfoSql = "INSERT INTO timing_data (name, host, timestamp, count, average, max, min) VALUES (?, ?, ?, ?, ?, ?, ?)";
try (PreparedStatement statement = conn.prepareStatement(addTimingInfoSql)) {
for(String metricName:metricNames) {
long baseIndex = rand.nextInt(900) + 100;
int multiplier = rand.nextInt(5);
long minIndexTimeNanos;
switch(multiplier) {
case 0:
minIndexTimeNanos = baseIndex;
break;
case 1:
minIndexTimeNanos = baseIndex * 1000;
break;
default:
minIndexTimeNanos = baseIndex * 1000 * 1000;
break;
}
long maxIndexTimeOverMin = minIndexTimeNanos * 3;
for(int node = 0;node < nNodes; node++) {
String host = "testHost" + node; // NON-NLS
double count = 0;
double maxCount = nDays * 24 + 1;
// Record data every hour, with a small amount of randomness about when it starts
for(long timestamp = minTimestamp + rand.nextInt(1000 * 60 * 55);timestamp < maxTimestamp;timestamp += millisPerHour) {
double aveTime;
// This creates data that increases in the last couple of days of the simulated
// collection
count++;
double slowNodeMultiplier = 1.0;
if((maxCount - count) <= 3 * 24) {
slowNodeMultiplier += (3 - (maxCount - count) / 24) * 0.33;
}
if( ! createVerificationData ) {
// Try to make a reasonable sample data set, with most points in a small range
// but some higher and lower
int outlierVal = rand.nextInt(30);
long randVal = rand.nextLong();
if(randVal < 0) {
randVal *= -1;
}
if(outlierVal < 2){
aveTime = minIndexTimeNanos + maxIndexTimeOverMin + randVal % maxIndexTimeOverMin;
} else if(outlierVal == 2){
aveTime = (minIndexTimeNanos / 2) + randVal % (minIndexTimeNanos / 2);
} else if(outlierVal < 17) {
aveTime = minIndexTimeNanos + randVal % (maxIndexTimeOverMin / 2);
} else {
aveTime = minIndexTimeNanos + randVal % maxIndexTimeOverMin;
}
if(node == 1) {
aveTime = aveTime * slowNodeMultiplier;
}
} else {
// Create a data set strictly for testing that the display is working
// correctly. The average time will equal the day of the month from
// the timestamp (in milliseconds)
Calendar thisDate = new GregorianCalendar();
thisDate.setTimeInMillis(timestamp);
int day = thisDate.get(Calendar.DAY_OF_MONTH);
aveTime = day * 1000000;
}
statement.setString(1, metricName);
statement.setString(2, host);
statement.setLong(3, timestamp);
statement.setLong(4, 0);
statement.setDouble(5, aveTime / 1000000);
statement.setDouble(6, 0);
statement.setDouble(7, 0);
statement.execute();
}
}
}
} catch (SQLException ex) {
throw new HealthMonitorException("Error saving metric data to database", ex);
}
} finally {
try {
if(conn != null) {
conn.close();
}
} catch (SQLException ex) {
logger.log(Level.SEVERE, "Error closing Connection.", ex);
}
try {
lock.release();
} catch (CoordinationService.CoordinationServiceException ex) {
throw new HealthMonitorException("Error releasing database lock", ex);
}
}
}
/**
* Get timing metrics currently stored in the database.
* @param timeRange Maximum age for returned metrics (in milliseconds)
* @return A map with metric name mapped to a list of data
* @throws HealthMonitorException
*/
Map<String, List<DatabaseTimingResult>> getTimingMetricsFromDatabase(long timeRange) throws HealthMonitorException {
// Make sure the monitor is enabled. It could theoretically get disabled after this
// check but it doesn't seem worth holding a lock to ensure that it doesn't since that
// may slow down ingest.
if(! isEnabled.get()) {
throw new HealthMonitorException("Health Monitor is not enabled");
}
// Calculate the smallest timestamp we should return
long minimumTimestamp = System.currentTimeMillis() - timeRange;
try (CoordinationService.Lock lock = getSharedDbLock()) {
if(lock == null) {
throw new HealthMonitorException("Error getting database lock");
}
Connection conn = connect();
if(conn == null) {
throw new HealthMonitorException("Error getting database connection");
}
Map<String, List<DatabaseTimingResult>> resultMap = new HashMap<>();
try (Statement statement = conn.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM timing_data WHERE timestamp > " + minimumTimestamp)) {
while (resultSet.next()) {
String name = resultSet.getString("name");
DatabaseTimingResult timingResult = new DatabaseTimingResult(resultSet);
if(resultMap.containsKey(name)) {
resultMap.get(name).add(timingResult);
} else {
List<DatabaseTimingResult> resultList = new ArrayList<>();
resultList.add(timingResult);
resultMap.put(name, resultList);
}
}
return resultMap;
} catch (SQLException ex) {
throw new HealthMonitorException("Error reading timing metrics from database", ex);
} finally {
try {
conn.close();
} catch (SQLException ex) {
logger.log(Level.SEVERE, "Error closing Connection.", ex);
}
}
} catch (CoordinationService.CoordinationServiceException ex) {
throw new HealthMonitorException("Error getting database lock", ex);
}
}
/**
* Get an exclusive lock for the health monitor database.
* Acquire this before creating, initializing, or updating the database schema.
@@ -856,4 +1162,74 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
return count;
}
}
/**
* Class for retrieving timing metrics from the database to display to the user.
* All times will be in milliseconds.
*/
static class DatabaseTimingResult {
private final long timestamp; // Time the metric was recorded
private final String hostname; // Host that recorded the metric
private final long count; // Number of metrics collected
private final double average; // Average of the durations collected (milliseconds)
private final double max; // Maximum value found (milliseconds)
private final double min; // Minimum value found (milliseconds)
DatabaseTimingResult(ResultSet resultSet) throws SQLException {
this.timestamp = resultSet.getLong("timestamp");
this.hostname = resultSet.getString("host");
this.count = resultSet.getLong("count");
this.average = resultSet.getDouble("average");
this.max = resultSet.getDouble("max");
this.min = resultSet.getDouble("min");
}
/**
* Get the timestamp for when the metric was recorded
* @return
*/
long getTimestamp() {
return timestamp;
}
/**
* Get the average duration
* @return average duration (milliseconds)
*/
double getAverage() {
return average;
}
/**
* Get the maximum duration
* @return maximum duration (milliseconds)
*/
double getMax() {
return max;
}
/**
* Get the minimum duration
* @return minimum duration (milliseconds)
*/
double getMin() {
return min;
}
/**
* Get the total number of metrics collected
* @return number of metrics collected
*/
long getCount() {
return count;
}
/**
* Get the name of the host that recorded this metric
* @return the host
*/
String getHostName() {
return hostname;
}
}
}
@@ -0,0 +1,513 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2018 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.healthmonitor;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.util.Set;
import java.util.HashSet;
import java.util.HashMap;
import java.util.Arrays;
import java.util.List;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JComboBox;
import javax.swing.JSeparator;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.BorderFactory;
import java.util.Map;
import javax.swing.BoxLayout;
import java.awt.GridLayout;
import java.nio.file.Paths;
import java.util.logging.Level;
import java.util.stream.Collectors;
import org.openide.modules.Places;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
/**
* Dashboard for viewing metrics and controlling the health monitor.
*/
public class HealthMonitorDashboard {
private final static Logger logger = Logger.getLogger(HealthMonitorDashboard.class.getName());
private final static String ADMIN_ACCESS_FILE_NAME = "adminAccess"; // NON-NLS
private final static String ADMIN_ACCESS_FILE_PATH = Paths.get(Places.getUserDirectory().getAbsolutePath(), ADMIN_ACCESS_FILE_NAME).toString();
Map<String, List<EnterpriseHealthMonitor.DatabaseTimingResult>> timingData;
private JComboBox<String> dateComboBox = null;
private JComboBox<String> hostComboBox = null;
private JCheckBox hostCheckBox = null;
private JCheckBox showTrendLineCheckBox = null;
private JCheckBox skipOutliersCheckBox = null;
private JPanel graphPanel = null;
private JDialog dialog = null;
private final Container parentWindow;
/**
* Create an instance of the dashboard.
* Call display() after creation to show the dashboard.
* @param parent The parent container (for centering the UI)
*/
public HealthMonitorDashboard(Container parent) {
timingData = new HashMap<>();
parentWindow = parent;
}
/**
* Display the dashboard.
*/
@NbBundle.Messages({"HealthMonitorDashboard.display.errorCreatingDashboard=Error creating health monitor dashboard",
"HealthMonitorDashboard.display.dashboardTitle=Enterprise Health Monitor"})
public void display() {
// Update the enabled status and get the timing data, then create all
// the sub panels.
JPanel timingPanel;
JPanel adminPanel;
try {
updateData();
timingPanel = createTimingPanel();
adminPanel = createAdminPanel();
} catch (HealthMonitorException ex) {
logger.log(Level.SEVERE, "Error creating panels for health monitor dashboard", ex);
MessageNotifyUtil.Message.error(Bundle.HealthMonitorDashboard_display_errorCreatingDashboard());
return;
}
// Create the main panel for the dialog
JPanel mainPanel = new JPanel();
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
// Add the timing panel
mainPanel.add(timingPanel);
// Add the admin panel if the admin file is present
File adminFile = new File(ADMIN_ACCESS_FILE_PATH);
if(adminFile.exists()) {
mainPanel.add(adminPanel);
}
// Create and show the dialog
dialog = new JDialog();
dialog.setTitle(Bundle.HealthMonitorDashboard_display_dashboardTitle());
dialog.add(mainPanel);
dialog.pack();
dialog.setLocationRelativeTo(parentWindow);
dialog.setVisible(true);
}
/**
* Delete the current dialog and create a new one. This should only be
* called after enabling or disabling the health monitor.
*/
private void redisplay() {
if (dialog != null) {
dialog.setVisible(false);
dialog.dispose();
}
display();
}
/**
* Check the monitor enabled status and, if enabled, get the timing data.
* @throws HealthMonitorException
*/
private void updateData() throws HealthMonitorException {
// Update the monitor status
EnterpriseHealthMonitor.getInstance().updateFromGlobalEnabledStatus();
if(EnterpriseHealthMonitor.monitorIsEnabled()) {
// Get a copy of the timing data from the database
timingData = EnterpriseHealthMonitor.getInstance().getTimingMetricsFromDatabase(DateRange.getMaximumTimestampRange());
}
}
/**
* Create the panel holding the timing graphs and the controls for them.
* @return The timing panel
* @throws HealthMonitorException
*/
@NbBundle.Messages({"HealthMonitorDashboard.createTimingPanel.noData=No data to display - monitor is not enabled",
"HealthMonitorDashboard.createTimingPanel.timingMetricsTitle=Timing Metrics"})
private JPanel createTimingPanel() throws HealthMonitorException {
// If the monitor isn't enabled, just add a message
if(! EnterpriseHealthMonitor.monitorIsEnabled()) {
//timingMetricPanel.setPreferredSize(new Dimension(400,100));
JPanel emptyTimingMetricPanel = new JPanel();
emptyTimingMetricPanel.add(new JLabel(Bundle.HealthMonitorDashboard_createTimingPanel_timingMetricsTitle()));
emptyTimingMetricPanel.add(new JLabel(" "));
emptyTimingMetricPanel.add(new JLabel(Bundle.HealthMonitorDashboard_createTimingPanel_noData()));
return emptyTimingMetricPanel;
}
JPanel timingMetricPanel = new JPanel();
timingMetricPanel.setLayout(new BoxLayout(timingMetricPanel, BoxLayout.PAGE_AXIS));
timingMetricPanel.setBorder(BorderFactory.createEtchedBorder());
// Add title
JLabel timingMetricTitle = new JLabel(Bundle.HealthMonitorDashboard_createTimingPanel_timingMetricsTitle());
timingMetricPanel.add(timingMetricTitle);
timingMetricPanel.add(new JSeparator());
// Add the controls
timingMetricPanel.add(createTimingControlPanel());
timingMetricPanel.add(new JSeparator());
// Create panel to hold graphs
graphPanel = new JPanel();
graphPanel.setLayout(new GridLayout(0,2));
// Update the graph panel, put it in a scroll pane, and add to the timing metric panel
updateTimingMetricGraphs();
JScrollPane scrollPane = new JScrollPane(graphPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
timingMetricPanel.add(scrollPane);
timingMetricPanel.revalidate();
timingMetricPanel.repaint();
return timingMetricPanel;
}
/**
* Create the panel with combo boxes for date range and host.
* @return the control panel
*/
@NbBundle.Messages({"HealthMonitorDashboard.createTimingControlPanel.filterByHost=Filter by host",
"HealthMonitorDashboard.createTimingControlPanel.maxDays=Max days to display",
"HealthMonitorDashboard.createTimingControlPanel.skipOutliers=Do not plot outliers",
"HealthMonitorDashboard.createTimingControlPanel.showTrendLine=Show trend line"})
private JPanel createTimingControlPanel() {
JPanel timingControlPanel = new JPanel();
// If the monitor is not enabled, don't add any components
if(! EnterpriseHealthMonitor.monitorIsEnabled()) {
return timingControlPanel;
}
// Create the combo box for selecting how much data to display
String[] dateOptionStrings = Arrays.stream(DateRange.values()).map(e -> e.getLabel()).toArray(String[]::new);
dateComboBox = new JComboBox<>(dateOptionStrings);
dateComboBox.setSelectedItem(DateRange.ONE_DAY.getLabel());
// Set up the listener on the date combo box
dateComboBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
try {
updateTimingMetricGraphs();
} catch (HealthMonitorException ex) {
logger.log(Level.SEVERE, "Error updating timing metric panel", ex);
}
}
});
// Create an array of host names
Set<String> hostNameSet = new HashSet<>();
for(String metricType:timingData.keySet()) {
for(EnterpriseHealthMonitor.DatabaseTimingResult result: timingData.get(metricType)) {
hostNameSet.add(result.getHostName());
}
}
// Load the host names into the combo box
hostComboBox = new JComboBox<>(hostNameSet.toArray(new String[hostNameSet.size()]));
// Set up the listener on the combo box
hostComboBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
try {
if((hostCheckBox != null) && hostCheckBox.isSelected()) {
updateTimingMetricGraphs();
}
} catch (HealthMonitorException ex) {
logger.log(Level.SEVERE, "Error populating timing metric panel", ex);
}
}
});
// Create the host checkbox
hostCheckBox = new JCheckBox(Bundle.HealthMonitorDashboard_createTimingControlPanel_filterByHost());
hostCheckBox.setSelected(false);
hostComboBox.setEnabled(false);
// Set up the listener on the checkbox
hostCheckBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
try {
hostComboBox.setEnabled(hostCheckBox.isSelected());
updateTimingMetricGraphs();
} catch (HealthMonitorException ex) {
logger.log(Level.SEVERE, "Error populating timing metric panel", ex);
}
}
});
// Create the checkbox for showing the trend line
showTrendLineCheckBox = new JCheckBox(Bundle.HealthMonitorDashboard_createTimingControlPanel_showTrendLine());
showTrendLineCheckBox.setSelected(true);
// Set up the listener on the checkbox
showTrendLineCheckBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
try {
updateTimingMetricGraphs();
} catch (HealthMonitorException ex) {
logger.log(Level.SEVERE, "Error populating timing metric panel", ex);
}
}
});
// Create the checkbox for omitting outliers
skipOutliersCheckBox = new JCheckBox(Bundle.HealthMonitorDashboard_createTimingControlPanel_skipOutliers());
skipOutliersCheckBox.setSelected(false);
// Set up the listener on the checkbox
skipOutliersCheckBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
try {
updateTimingMetricGraphs();
} catch (HealthMonitorException ex) {
logger.log(Level.SEVERE, "Error populating timing metric panel", ex);
}
}
});
// Add the date range combo box and label to the panel
timingControlPanel.add(new JLabel(Bundle.HealthMonitorDashboard_createTimingControlPanel_maxDays()));
timingControlPanel.add(dateComboBox);
// Put some space between the elements
timingControlPanel.add(Box.createHorizontalStrut(100));
// Add the host combo box and checkbox to the panel
timingControlPanel.add(hostCheckBox);
timingControlPanel.add(hostComboBox);
// Put some space between the elements
timingControlPanel.add(Box.createHorizontalStrut(100));
// Add the skip outliers checkbox
timingControlPanel.add(this.showTrendLineCheckBox);
// Put some space between the elements
timingControlPanel.add(Box.createHorizontalStrut(100));
// Add the skip outliers checkbox
timingControlPanel.add(this.skipOutliersCheckBox);
return timingControlPanel;
}
/**
* Update the timing graphs.
* @throws HealthMonitorException
*/
@NbBundle.Messages({"HealthMonitorDashboard.updateTimingMetricGraphs.noData=No data to display"})
private void updateTimingMetricGraphs() throws HealthMonitorException {
// Clear out any old graphs
graphPanel.removeAll();
if(timingData.keySet().isEmpty()) {
// There are no timing metrics in the database
graphPanel.add(new JLabel(Bundle.HealthMonitorDashboard_updateTimingMetricGraphs_noData()));
return;
}
for(String metricName:timingData.keySet()) {
// If necessary, trim down the list of results to fit the selected time range
List<EnterpriseHealthMonitor.DatabaseTimingResult> intermediateTimingDataForDisplay;
if(dateComboBox.getSelectedItem() != null) {
DateRange selectedDateRange = DateRange.fromLabel(dateComboBox.getSelectedItem().toString());
long threshold = System.currentTimeMillis() - selectedDateRange.getTimestampRange();
intermediateTimingDataForDisplay = timingData.get(metricName).stream()
.filter(t -> t.getTimestamp() > threshold)
.collect(Collectors.toList());
} else {
intermediateTimingDataForDisplay = timingData.get(metricName);
}
// Get the name of the selected host, if there is one.
// The graph always uses the data from all hosts to generate the x and y scales
// so we don't filter anything out here.
String hostToDisplay = null;
if(hostCheckBox.isSelected() && (hostComboBox.getSelectedItem() != null)) {
hostToDisplay = hostComboBox.getSelectedItem().toString();
}
// Generate the graph
TimingMetricGraphPanel singleTimingGraphPanel = new TimingMetricGraphPanel(intermediateTimingDataForDisplay,
hostToDisplay, true, metricName, skipOutliersCheckBox.isSelected(), showTrendLineCheckBox.isSelected());
singleTimingGraphPanel.setPreferredSize(new Dimension(700,200));
graphPanel.add(singleTimingGraphPanel);
}
graphPanel.revalidate();
graphPanel.repaint();
}
/**
* Create the admin panel.
* This allows the health monitor to be enabled and disabled.
* @return
*/
@NbBundle.Messages({"HealthMonitorDashboard.createAdminPanel.enableButton=Enable monitor",
"HealthMonitorDashboard.createAdminPanel.disableButton=Disable monitor"})
private JPanel createAdminPanel() {
JPanel adminPanel = new JPanel();
adminPanel.setBorder(BorderFactory.createEtchedBorder());
// Create the buttons for enabling/disabling the monitor
JButton enableButton = new JButton(Bundle.HealthMonitorDashboard_createAdminPanel_enableButton());
JButton disableButton = new JButton(Bundle.HealthMonitorDashboard_createAdminPanel_disableButton());
boolean isEnabled = EnterpriseHealthMonitor.monitorIsEnabled();
enableButton.setEnabled(! isEnabled);
disableButton.setEnabled(isEnabled);
// Set up a listener on the enable button
enableButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
try {
dialog.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
EnterpriseHealthMonitor.setEnabled(true);
redisplay();
} catch (HealthMonitorException ex) {
logger.log(Level.SEVERE, "Error enabling monitoring", ex);
} finally {
dialog.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
}
});
// Set up a listener on the disable button
disableButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
try {
dialog.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
EnterpriseHealthMonitor.setEnabled(false);
redisplay();
} catch (HealthMonitorException ex) {
logger.log(Level.SEVERE, "Error disabling monitoring", ex);
} finally {
dialog.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
}
});
// Add the buttons
adminPanel.add(enableButton);
adminPanel.add(Box.createHorizontalStrut(25));
adminPanel.add(disableButton);
return adminPanel;
}
/**
* Possible date ranges for the metrics in the UI
*/
@NbBundle.Messages({"HealthMonitorDashboard.DateRange.oneMonth=One month",
"HealthMonitorDashboard.DateRange.twoWeeks=Two weeks",
"HealthMonitorDashboard.DateRange.oneWeek=One week",
"HealthMonitorDashboard.DateRange.oneDay=One day"})
private enum DateRange {
ONE_DAY(Bundle.HealthMonitorDashboard_DateRange_oneDay(), 1),
ONE_WEEK(Bundle.HealthMonitorDashboard_DateRange_oneWeek(), 7),
TWO_WEEKS(Bundle.HealthMonitorDashboard_DateRange_twoWeeks(), 14),
ONE_MONTH(Bundle.HealthMonitorDashboard_DateRange_oneMonth(), 31);
private final String label;
private final long numberOfDays;
private static final long MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;
DateRange(String label, long numberOfDays) {
this.label = label;
this.numberOfDays = numberOfDays;
}
/**
* Get the name for display in the UI
* @return the name
*/
String getLabel() {
return label;
}
/**
* Get the number of milliseconds represented by this date range.
* Compare the timestamps to ((current time in millis) - (this value)) to
* determine if they are in the range
* @return the time range in milliseconds
*/
long getTimestampRange() {
if (numberOfDays > 0) {
return numberOfDays * MILLISECONDS_PER_DAY;
} else {
return Long.MAX_VALUE;
}
}
/**
* Get the maximum range for this enum.
* This should be used for querying the database for the timing metrics to display.
* @return the maximum range in milliseconds
*/
static long getMaximumTimestampRange() {
long maxRange = Long.MIN_VALUE;
for (DateRange dateRange : DateRange.values()) {
if (dateRange.getTimestampRange() > maxRange) {
maxRange = dateRange.getTimestampRange();
}
}
return maxRange;
}
static DateRange fromLabel(String text) {
for (DateRange dateRange : DateRange.values()) {
if (dateRange.label.equalsIgnoreCase(text)) {
return dateRange;
}
}
return ONE_DAY; // If the comparison failed, return a default
}
}
}
@@ -0,0 +1,510 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2018 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.healthmonitor;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.util.Collections;
import java.util.stream.Collectors;
import java.util.Comparator;
import java.util.ArrayList;
import java.util.List;
import java.util.Calendar;
import java.util.GregorianCalendar;
import javax.swing.JPanel;
import org.sleuthkit.autopsy.coreutils.Logger;
import java.util.logging.Level;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.healthmonitor.EnterpriseHealthMonitor.DatabaseTimingResult;
/**
* Creates a graph of the given timing metric data
*/
class TimingMetricGraphPanel extends JPanel {
private final static Logger logger = Logger.getLogger(TimingMetricGraphPanel.class.getName());
private final int padding = 25;
private final int labelPadding = 25;
private final Color lineColor = new Color(0x12, 0x20, 0xdb, 180);
private final Color gridColor = new Color(200, 200, 200, 200);
private final Color trendLineColor = new Color(150, 10, 10, 200);
private static final Stroke GRAPH_STROKE = new BasicStroke(2f);
private static final Stroke NARROW_STROKE = new BasicStroke(1f);
private final int pointWidth = 4;
private final int numberYDivisions = 10;
private List<DatabaseTimingResult> timingResults;
private final String metricName;
private final boolean doLineGraph;
private final boolean skipOutliers;
private final boolean showTrendLine;
private String yUnitString;
private TrendLine trendLine;
private final long MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;
private final long NANOSECONDS_PER_MILLISECOND = 1000 * 1000;
private long maxTimestamp;
private long minTimestamp;
private double maxMetricTime;
private double minMetricTime;
TimingMetricGraphPanel(List<DatabaseTimingResult> timingResultsFull,
String hostName, boolean doLineGraph, String metricName, boolean skipOutliers, boolean showTrendLine) {
this.doLineGraph = doLineGraph;
this.skipOutliers = skipOutliers;
this.showTrendLine = showTrendLine;
this.metricName = metricName;
if(hostName == null || hostName.isEmpty()) {
timingResults = timingResultsFull;
} else {
timingResults = timingResultsFull.stream()
.filter(t -> t.getHostName().equals(hostName))
.collect(Collectors.toList());
}
if(showTrendLine) {
try {
trendLine = new TrendLine(timingResults);
} catch (HealthMonitorException ex) {
// Log it, set trendLine to null and continue on
logger.log(Level.WARNING, "Can not generate a trend line on empty data set");
trendLine = null;
}
}
// Calculate these using the full data set, to make it easier to compare the results for
// individual hosts. Calculate the average at the same time.
maxMetricTime = Double.MIN_VALUE;
minMetricTime = Double.MAX_VALUE;
maxTimestamp = Long.MIN_VALUE;
minTimestamp = Long.MAX_VALUE;
double averageMetricTime = 0.0;
for (DatabaseTimingResult result : timingResultsFull) {
maxMetricTime = Math.max(maxMetricTime, result.getAverage());
minMetricTime = Math.min(minMetricTime, result.getAverage());
maxTimestamp = Math.max(maxTimestamp, result.getTimestamp());
minTimestamp = Math.min(minTimestamp, result.getTimestamp());
averageMetricTime += result.getAverage();
}
averageMetricTime = averageMetricTime / timingResultsFull.size();
// If we're omitting outliers, we may use a different maxMetricTime.
// If the max time is reasonably close to the average, do nothing
if (this.skipOutliers && (maxMetricTime > (averageMetricTime * 5))) {
// Calculate the standard deviation
double intermediateValue = 0.0;
for (DatabaseTimingResult result : timingResultsFull) {
double diff = result.getAverage() - averageMetricTime;
intermediateValue += diff * diff;
}
double standardDeviation = Math.sqrt(intermediateValue / timingResultsFull.size());
maxMetricTime = averageMetricTime + standardDeviation;
}
}
/**
* Setup of the graphics panel:
* Origin (0,0) is at the top left corner
*
* Horizontally (from the left): (padding)(label padding)(the graph)(padding)
* For plotting data on the x-axis, we scale it to the size of the graph and then add the padding and label padding
*
* Vertically (from the top): (padding)(the graph)(label padding)(padding)
* For plotting data on the y-axis, we subtract from the max value in the graph and then scale to the size of the graph
* @param g
*/
@NbBundle.Messages({"TimingMetricGraphPanel.paintComponent.nanoseconds=nanoseconds",
"TimingMetricGraphPanel.paintComponent.microseconds=microseconds",
"TimingMetricGraphPanel.paintComponent.milliseconds=milliseconds",
"TimingMetricGraphPanel.paintComponent.seconds=seconds",
"TimingMetricGraphPanel.paintComponent.minutes=minutes",
"TimingMetricGraphPanel.paintComponent.hours=hours",
"TimingMetricGraphPanel.paintComponent.displayingTime=displaying time in "})
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Get the max and min timestamps to create the x-axis.
// We add a small buffer to each side so the data won't overwrite the axes.
double maxValueOnXAxis = maxTimestamp + TimeUnit.HOURS.toMillis(2); // Two hour buffer
double minValueOnXAxis = minTimestamp - TimeUnit.HOURS.toMillis(2); // Two hour buffer
// Get the max and min times to create the y-axis
// We add a small buffer to each side so the data won't overwrite the axes.
double maxValueOnYAxis = maxMetricTime;
double minValueOnYAxis = minMetricTime;
minValueOnYAxis = Math.max(0, minValueOnYAxis - (maxValueOnYAxis * 0.1));
maxValueOnYAxis = maxValueOnYAxis * 1.1;
// The graph itself has the following corners:
// (padding + label padding, padding + font height) -> top left
// (padding + label padding, getHeight() - label padding - padding) -> bottom left
// (getWidth() - padding, padding + font height) -> top right
// (padding + label padding, getHeight() - label padding - padding) -> bottom right
int leftGraphPadding = padding + labelPadding;
int rightGraphPadding = padding;
int topGraphPadding = padding + g2.getFontMetrics().getHeight();
int bottomGraphPadding = labelPadding;
// Calculate the scale for each axis.
// The size of the graph area is the width/height of the panel minus any padding.
// The scale is calculated based on this size of the graph compared to the data range.
// For example:
// getWidth() = 575 => graph width = 500
// If our max x value to plot is 10000 and our min is 0, then the xScale would be 0.05 - i.e.,
// our original x values will be multipled by 0.05 to translate them to an x-coordinate in the
// graph (plus the padding)
int graphWidth = getWidth() - leftGraphPadding - rightGraphPadding;
int graphHeight = getHeight() - topGraphPadding - bottomGraphPadding;
double xScale = ((double) graphWidth) / (maxValueOnXAxis - minValueOnXAxis);
double yScale = ((double) graphHeight) / (maxValueOnYAxis - minValueOnYAxis);
// Check if we should use a scale other than milliseconds
// The idea here is to pick the scale that would most commonly be used to
// represent the middle of our data. For example, if the middle of the graph
// would be 45,000,000 nanoseconds, then we would use milliseconds for the
// y-axis.
long middleOfGraphNano = (long)((minValueOnYAxis + (maxValueOnYAxis - minValueOnYAxis) / 2.0) * NANOSECONDS_PER_MILLISECOND);
double yLabelScale;
if(middleOfGraphNano < TimeUnit.MICROSECONDS.toNanos(1)) {
yUnitString = Bundle.TimingMetricGraphPanel_paintComponent_nanoseconds();
yLabelScale = TimeUnit.MILLISECONDS.toNanos(1);
} else if (TimeUnit.NANOSECONDS.toMicros(middleOfGraphNano) < TimeUnit.MILLISECONDS.toMicros(1)) {
yUnitString = Bundle.TimingMetricGraphPanel_paintComponent_microseconds();
yLabelScale = TimeUnit.MILLISECONDS.toMicros(1);
} else if (TimeUnit.NANOSECONDS.toMillis(middleOfGraphNano) < TimeUnit.SECONDS.toMillis(1)) {
yUnitString = Bundle.TimingMetricGraphPanel_paintComponent_milliseconds();
yLabelScale = 1;
} else if (TimeUnit.NANOSECONDS.toSeconds(middleOfGraphNano) < TimeUnit.MINUTES.toSeconds(1)) {
yUnitString = Bundle.TimingMetricGraphPanel_paintComponent_seconds();
yLabelScale = 1.0 / TimeUnit.SECONDS.toMillis(1);
} else if (TimeUnit.NANOSECONDS.toMinutes(middleOfGraphNano) < TimeUnit.HOURS.toMinutes(1)) {
yUnitString = Bundle.TimingMetricGraphPanel_paintComponent_minutes();
yLabelScale = 1.0 / (TimeUnit.MINUTES.toMillis(1));
} else {
yUnitString = Bundle.TimingMetricGraphPanel_paintComponent_hours();
yLabelScale = 1.0 / (TimeUnit.HOURS.toMillis(1));
}
// Draw white background
g2.setColor(Color.WHITE);
g2.fillRect(leftGraphPadding, topGraphPadding, graphWidth, graphHeight);
// Create hatch marks and grid lines for y axis.
int labelWidth;
int positionForMetricNameLabel = 0;
for (int i = 0; i < numberYDivisions + 1; i++) {
int x0 = leftGraphPadding;
int x1 = pointWidth + leftGraphPadding;
int y0 = getHeight() - ((i * graphHeight) / numberYDivisions + bottomGraphPadding);
int y1 = y0;
if ( ! timingResults.isEmpty()) {
// Draw the grid line
g2.setColor(gridColor);
g2.drawLine(leftGraphPadding + 1 + pointWidth, y0, getWidth() - rightGraphPadding, y1);
// Create the label
g2.setColor(Color.BLACK);
double yValue = minValueOnYAxis + ((maxValueOnYAxis - minValueOnYAxis) * ((i * 1.0) / numberYDivisions));
String yLabel = Double.toString(((int) (yValue * 100 * yLabelScale)) / 100.0);
FontMetrics fontMetrics = g2.getFontMetrics();
labelWidth = fontMetrics.stringWidth(yLabel);
g2.drawString(yLabel, x0 - labelWidth - 5, y0 + (fontMetrics.getHeight() / 2) - 3);
// The nicest looking alignment for this label seems to be left-aligned with the top
// y-axis label. Save this position to be used to write the label later.
if (i == numberYDivisions) {
positionForMetricNameLabel = x0 - labelWidth - 5;
}
}
// Draw the small hatch mark
g2.setColor(Color.BLACK);
g2.drawLine(x0, y0, x1, y1);
}
// On the x-axis, the farthest right grid line should represent midnight preceding the last recorded value
Calendar maxDate = new GregorianCalendar();
maxDate.setTimeInMillis(maxTimestamp);
maxDate.set(Calendar.HOUR_OF_DAY, 0);
maxDate.set(Calendar.MINUTE, 0);
maxDate.set(Calendar.SECOND, 0);
maxDate.set(Calendar.MILLISECOND, 0);
long maxMidnightInMillis = maxDate.getTimeInMillis();
// We don't want to display more than 20 grid lines. If we have more
// data then that, put multiple days within one division
long totalDays = (maxMidnightInMillis - (long)minValueOnXAxis) / MILLISECONDS_PER_DAY;
long daysPerDivision;
if(totalDays <= 20) {
daysPerDivision = 1;
} else {
daysPerDivision = (totalDays / 20);
if((totalDays % 20) != 0) {
daysPerDivision++;
}
}
// Draw the vertical grid lines and labels
// The vertical grid lines will be at midnight, and display the date underneath them
// At present we use GMT because of some complications with daylight savings time.
for (long currentDivision = maxMidnightInMillis; currentDivision >= minValueOnXAxis; currentDivision -= MILLISECONDS_PER_DAY * daysPerDivision) {
int x0 = (int) ((currentDivision - minValueOnXAxis) * xScale + leftGraphPadding);
int x1 = x0;
int y0 = getHeight() - bottomGraphPadding;
int y1 = y0 - pointWidth;
// Draw the light grey grid line
g2.setColor(gridColor);
g2.drawLine(x0, getHeight() - bottomGraphPadding - 1 - pointWidth, x1, topGraphPadding);
// Draw the hatch mark
g2.setColor(Color.BLACK);
g2.drawLine(x0, y0, x1, y1);
// Draw the label
Calendar thisDate = new GregorianCalendar();
thisDate.setTimeZone(TimeZone.getTimeZone("GMT")); // Stick with GMT to avoid daylight savings issues
thisDate.setTimeInMillis(currentDivision);
int month = thisDate.get(Calendar.MONTH) + 1;
int day = thisDate.get(Calendar.DAY_OF_MONTH);
String xLabel = month + "/" + day;
FontMetrics metrics = g2.getFontMetrics();
labelWidth = metrics.stringWidth(xLabel);
g2.drawString(xLabel, x0 - labelWidth / 2, y0 + metrics.getHeight() + 3);
}
// Create x and y axes
g2.setColor(Color.BLACK);
g2.drawLine(leftGraphPadding, getHeight() - bottomGraphPadding, leftGraphPadding, topGraphPadding);
g2.drawLine(leftGraphPadding, getHeight() - bottomGraphPadding, getWidth() - rightGraphPadding, getHeight() - bottomGraphPadding);
// Create the points to plot
List<Point> graphPoints = new ArrayList<>();
for (int i = 0; i < timingResults.size(); i++) {
double metricTime = timingResults.get(i).getAverage();
int x1 = (int) ((timingResults.get(i).getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding);
int y1 = (int) ((maxValueOnYAxis - metricTime) * yScale + topGraphPadding);
graphPoints.add(new Point(x1, y1));
}
// Sort the points
Collections.sort(graphPoints, new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
if(o1.getX() > o2.getX()) {
return 1;
} else if (o1.getX() < o2.getX()) {
return -1;
}
return 0;
}
});
// Draw the selected type of graph. If there's only one data point,
// draw that single point.
g2.setStroke(NARROW_STROKE);
g2.setColor(lineColor);
if(doLineGraph && graphPoints.size() > 1) {
for (int i = 0; i < graphPoints.size() - 1; i++) {
int x1 = graphPoints.get(i).x;
int y1 = graphPoints.get(i).y;
int x2 = graphPoints.get(i + 1).x;
int y2 = graphPoints.get(i + 1).y;
g2.drawLine(x1, y1, x2, y2);
}
} else {
for (int i = 0; i < graphPoints.size(); i++) {
int x = graphPoints.get(i).x - pointWidth / 2;
int y = graphPoints.get(i).y - pointWidth / 2;
int ovalW = pointWidth;
int ovalH = pointWidth;
g2.fillOval(x, y, ovalW, ovalH);
}
}
// Draw the trend line.
// Don't draw anything if we don't have at least two data points.
if(showTrendLine && (trendLine != null) && (timingResults.size() > 1)) {
double x0value = minValueOnXAxis;
double y0value = trendLine.getExpectedValueAt(x0value);
if (y0value < minValueOnYAxis) {
try {
y0value = minValueOnYAxis;
x0value = trendLine.getXGivenY(y0value);
} catch (HealthMonitorException ex) {
// The exception is caused by a slope of zero on the trend line, which
// shouldn't be able to happen at the same time as having a trend line that dips below the y-axis.
// If it does, log a warning but continue on with the original values.
logger.log(Level.WARNING, "Error plotting trend line", ex);
}
} else if (y0value > maxValueOnYAxis) {
try {
y0value = maxValueOnYAxis;
x0value = trendLine.getXGivenY(y0value);
} catch (HealthMonitorException ex) {
// The exception is caused by a slope of zero on the trend line, which
// shouldn't be able to happen at the same time as having a trend line that dips below the y-axis.
// If it does, log a warning but continue on with the original values.
logger.log(Level.WARNING, "Error plotting trend line", ex);
}
}
int x0 = (int) ((x0value - minValueOnXAxis) * xScale) + leftGraphPadding;
int y0 = (int) ((maxValueOnYAxis - y0value) * yScale + topGraphPadding);
double x1value = maxValueOnXAxis;
double y1value = trendLine.getExpectedValueAt(maxValueOnXAxis);
if (y1value < minValueOnYAxis) {
try {
y1value = minValueOnYAxis;
x1value = trendLine.getXGivenY(y1value);
} catch (HealthMonitorException ex) {
// The exception is caused by a slope of zero on the trend line, which
// shouldn't be able to happen at the same time as having a trend line that dips below the y-axis.
// If it does, log a warning but continue on with the original values.
logger.log(Level.WARNING, "Error plotting trend line", ex);
}
} else if (y1value > maxValueOnYAxis) {
try {
y1value = maxValueOnYAxis;
x1value = trendLine.getXGivenY(y1value);
} catch (HealthMonitorException ex) {
// The exception is caused by a slope of zero on the trend line, which
// shouldn't be able to happen at the same time as having a trend line that dips below the y-axis.
// If it does, log a warning but continue on with the original values.
logger.log(Level.WARNING, "Error plotting trend line", ex);
}
}
int x1 = (int) ((x1value - minValueOnXAxis) * xScale) + leftGraphPadding;
int y1 = (int) ((maxValueOnYAxis - y1value) * yScale + topGraphPadding);
g2.setStroke(GRAPH_STROKE);
g2.setColor(trendLineColor);
g2.drawLine(x0, y0, x1, y1);
}
// The graph lines may have extended up past the bounds of the graph. Overwrite that
// area with the original background color.
g2.setColor(this.getBackground());
g2.fillRect(leftGraphPadding, 0, graphWidth, topGraphPadding);
// Write the scale. Do this after we erase the top block of the graph.
g2.setColor(Color.BLACK);
String scaleStr = Bundle.TimingMetricGraphPanel_paintComponent_displayingTime() + yUnitString;
String titleStr = metricName + " - " + scaleStr;
g2.drawString(titleStr, positionForMetricNameLabel, padding);
}
/**
* Class to generate a linear trend line from timing metric data.
*
* Formula for the linear trend line:
* (x,y) = (timestamp, metric time)
* n = total number of metrics
*
* slope = ( n * Σ(xy) - Σx * Σy ) / ( n * Σ(x^2) - (Σx)^2 )
*
* y intercept = ( Σy - (slope) * Σx ) / n
*/
private class TrendLine {
double slope;
double yInt;
TrendLine(List<DatabaseTimingResult> timingResults) throws HealthMonitorException {
if((timingResults == null) || timingResults.isEmpty()) {
throw new HealthMonitorException("Can not generate trend line for empty/null data set");
}
// Calculate intermediate values
int n = timingResults.size();
double sumX = 0;
double sumY = 0;
double sumXY = 0;
double sumXsquared = 0;
for(int i = 0;i < n;i++) {
double x = timingResults.get(i).getTimestamp();
double y = timingResults.get(i).getAverage();
sumX += x;
sumY += y;
sumXY += x * y;
sumXsquared += x * x;
}
// Calculate slope
// With only one measurement, the denominator will end being zero in the formula.
// Use a horizontal line in this case (or any case where the denominator is zero)
double denominator = n * sumXsquared - sumX * sumX;
if (denominator != 0) {
slope = (n * sumXY - sumX * sumY) / denominator;
} else {
slope = 0;
}
// Calculate y intercept
yInt = (sumY - slope * sumX) / n;
}
/**
* Get the expected y value for a given x
* @param x x coordinate of the point on the trend line
* @return expected y coordinate of this point on the trend line
*/
double getExpectedValueAt(double x) {
return (slope * x + yInt);
}
/**
* Get the x-coordinate for a given Y-coordinate.
* Should only be necessary when the trend line does not fit on the graph
* @param y the y coordinate
* @return expected x coordinate for the given y
* @throws HealthMonitorException
*/
double getXGivenY(double y) throws HealthMonitorException {
if (slope != 0.0) {
return ((y - yInt) / slope);
} else {
throw new HealthMonitorException("Attempted division by zero in trend line calculation");
}
}
}
}
@@ -18,6 +18,7 @@
*/
package org.sleuthkit.autopsy.ingest;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@@ -37,6 +38,9 @@ import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.NetworkUtils;
import org.sleuthkit.autopsy.ingest.DataSourceIngestPipeline.PipelineModule;
import org.sleuthkit.autopsy.ingest.IngestJob.CancellationReason;
import org.sleuthkit.autopsy.ingest.IngestTasksScheduler.IngestJobTasksSnapshot;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.IngestJobInfo;
@@ -51,7 +55,7 @@ import org.sleuthkit.autopsy.modules.interestingitems.FilesSet;
* Encapsulates a data source and the ingest module pipelines used to process
* it.
*/
final class DataSourceIngestJob {
public final class DataSourceIngestJob {
private static final Logger logger = Logger.getLogger(DataSourceIngestJob.class.getName());
@@ -1079,71 +1083,90 @@ final class DataSourceIngestJob {
* @return An ingest job statistics object.
*/
Snapshot getSnapshot(boolean getIngestTasksSnapshot) {
return new Snapshot(getIngestTasksSnapshot);
/**
* Determine whether file ingest is running at the time of this snapshot
* and determine the earliest file ingest level pipeline start time, if
* file ingest was started at all.
*/
boolean fileIngestRunning = false;
Date fileIngestStartTime = null;
for (FileIngestPipeline pipeline : this.fileIngestPipelines) {
if (pipeline.isRunning()) {
fileIngestRunning = true;
}
Date pipelineStartTime = pipeline.getStartTime();
if (null != pipelineStartTime && (null == fileIngestStartTime || pipelineStartTime.before(fileIngestStartTime))) {
fileIngestStartTime = pipelineStartTime;
}
}
long processedFilesCount = 0;
long estimatedFilesToProcessCount = 0;
long snapShotTime = new Date().getTime();
IngestJobTasksSnapshot tasksSnapshot = null;
if (getIngestTasksSnapshot) {
synchronized (fileIngestProgressLock) {
processedFilesCount = this.processedFiles;
estimatedFilesToProcessCount = this.estimatedFilesToProcess;
snapShotTime = new Date().getTime();
}
tasksSnapshot = DataSourceIngestJob.taskScheduler.getTasksSnapshotForJob(id);
}
return new Snapshot(this.dataSource.getName(), id, createTime,
getCurrentDataSourceIngestModule(), fileIngestRunning, fileIngestStartTime,
cancelled, cancellationReason, cancelledDataSourceIngestModules,
processedFilesCount, estimatedFilesToProcessCount, snapShotTime, tasksSnapshot);
}
/**
* Stores basic diagnostic statistics for a data source ingest job.
*/
final class Snapshot {
public static final class Snapshot implements Serializable {
private static final long serialVersionUID = 1L;
private final String dataSource;
private final long jobId;
private final long jobStartTime;
private final long snapShotTime;
private final DataSourceIngestPipeline.PipelineModule dataSourceLevelIngestModule;
private boolean fileIngestRunning;
private Date fileIngestStartTime;
transient private final PipelineModule dataSourceLevelIngestModule;
private final boolean fileIngestRunning;
private final Date fileIngestStartTime;
private final long processedFiles;
private final long estimatedFilesToProcess;
private final IngestTasksScheduler.IngestJobTasksSnapshot tasksSnapshot;
private final boolean jobCancelled;
private final IngestJob.CancellationReason jobCancellationReason;
private final List<String> cancelledDataSourceModules;
private final IngestJobTasksSnapshot tasksSnapshot;
transient private final boolean jobCancelled;
transient private final CancellationReason jobCancellationReason;
transient private final List<String> cancelledDataSourceModules;
/**
* Constructs an object to store basic diagnostic statistics for a data
* source ingest job.
*/
Snapshot(boolean getIngestTasksSnapshot) {
this.dataSource = DataSourceIngestJob.this.dataSource.getName();
this.jobId = DataSourceIngestJob.this.id;
this.jobStartTime = DataSourceIngestJob.this.createTime;
this.dataSourceLevelIngestModule = DataSourceIngestJob.this.getCurrentDataSourceIngestModule();
Snapshot(String dataSourceName, long jobId, long jobStartTime, PipelineModule dataSourceIngestModule,
boolean fileIngestRunning, Date fileIngestStartTime,
boolean jobCancelled, CancellationReason cancellationReason, List<String> cancelledModules,
long processedFiles, long estimatedFilesToProcess,
long snapshotTime, IngestJobTasksSnapshot tasksSnapshot) {
this.dataSource = dataSourceName;
this.jobId = jobId;
this.jobStartTime = jobStartTime;
this.dataSourceLevelIngestModule = dataSourceIngestModule;
/**
* Determine whether file ingest is running at the time of this
* snapshot and determine the earliest file ingest level pipeline
* start time, if file ingest was started at all.
*/
for (FileIngestPipeline pipeline : DataSourceIngestJob.this.fileIngestPipelines) {
if (pipeline.isRunning()) {
this.fileIngestRunning = true;
}
Date pipelineStartTime = pipeline.getStartTime();
if (null != pipelineStartTime && (null == this.fileIngestStartTime || pipelineStartTime.before(this.fileIngestStartTime))) {
this.fileIngestStartTime = pipelineStartTime;
}
}
this.jobCancelled = cancelled;
this.fileIngestRunning = fileIngestRunning;
this.fileIngestStartTime = fileIngestStartTime;
this.jobCancelled = jobCancelled;
this.jobCancellationReason = cancellationReason;
this.cancelledDataSourceModules = new ArrayList<>(DataSourceIngestJob.this.cancelledDataSourceIngestModules);
this.cancelledDataSourceModules = cancelledModules;
if (getIngestTasksSnapshot) {
synchronized (DataSourceIngestJob.this.fileIngestProgressLock) {
this.processedFiles = DataSourceIngestJob.this.processedFiles;
this.estimatedFilesToProcess = DataSourceIngestJob.this.estimatedFilesToProcess;
this.snapShotTime = new Date().getTime();
}
this.tasksSnapshot = DataSourceIngestJob.taskScheduler.getTasksSnapshotForJob(this.jobId);
} else {
this.processedFiles = 0;
this.estimatedFilesToProcess = 0;
this.snapShotTime = new Date().getTime();
this.tasksSnapshot = null;
}
this.processedFiles = processedFiles;
this.estimatedFilesToProcess = estimatedFilesToProcess;
this.snapShotTime = snapshotTime;
this.tasksSnapshot = tasksSnapshot;
}
/**
@@ -1190,11 +1213,11 @@ final class DataSourceIngestJob {
return this.dataSourceLevelIngestModule;
}
boolean fileIngestIsRunning() {
boolean getFileIngestIsRunning() {
return this.fileIngestRunning;
}
Date fileIngestStartTime() {
Date getFileIngestStartTime() {
return this.fileIngestStartTime;
}
@@ -355,10 +355,10 @@ public final class IngestJob {
dataSourceModule = new DataSourceIngestModuleHandle(dataSourceJobs.get(snapshot.getJobId()), module);
}
}
if (snapshot.fileIngestIsRunning()) {
if (snapshot.getFileIngestIsRunning()) {
fileIngestRunning = true;
}
Date childFileIngestStartTime = snapshot.fileIngestStartTime();
Date childFileIngestStartTime = snapshot.getFileIngestStartTime();
if (null != childFileIngestStartTime && (null == fileIngestStartTime || childFileIngestStartTime.before(fileIngestStartTime))) {
fileIngestStartTime = childFileIngestStartTime;
}
@@ -22,6 +22,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.awt.EventQueue;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -107,7 +108,7 @@ import org.sleuthkit.datamodel.Content;
* job progress, and ingest module run times.
*/
@ThreadSafe
public class IngestManager {
public class IngestManager implements IngestProgressSnapshotProvider {
private final static Logger logger = Logger.getLogger(IngestManager.class.getName());
private final static String INGEST_JOB_EVENT_CHANNEL_NAME = "%s-Ingest-Job-Events"; //NON-NLS
@@ -756,7 +757,8 @@ public class IngestManager {
*
* @return Map of module name to run time (in milliseconds)
*/
Map<String, Long> getModuleRunTimes() {
@Override
public Map<String, Long> getModuleRunTimes() {
synchronized (ingestModuleRunTimes) {
Map<String, Long> times = new HashMap<>(ingestModuleRunTimes);
return times;
@@ -769,7 +771,8 @@ public class IngestManager {
*
* @return A collection of ingest manager ingest task snapshots.
*/
List<IngestThreadActivitySnapshot> getIngestThreadActivitySnapshots() {
@Override
public List<IngestThreadActivitySnapshot> getIngestThreadActivitySnapshots() {
return new ArrayList<>(ingestThreadActivitySnapshots.values());
}
@@ -778,7 +781,8 @@ public class IngestManager {
*
* @return A list of ingest job state snapshots.
*/
List<DataSourceIngestJob.Snapshot> getIngestJobSnapshots() {
@Override
public List<DataSourceIngestJob.Snapshot> getIngestJobSnapshots() {
List<DataSourceIngestJob.Snapshot> snapShots = new ArrayList<>();
synchronized (ingestJobsById) {
ingestJobsById.values().forEach((job) -> {
@@ -916,7 +920,9 @@ public class IngestManager {
* running in an ingest thread.
*/
@Immutable
static final class IngestThreadActivitySnapshot {
public static final class IngestThreadActivitySnapshot implements Serializable {
private static final long serialVersionUID = 1L;
private final long threadId;
private final Date startTime;
@@ -50,12 +50,14 @@ public final class IngestProgressSnapshotDialog extends JDialog {
/**
* Constructs an instance of the dialog with its own frame. Could be modal.
* Uses the given provider as the source of data for the dialog.
*
* @param owner - the owner of this dialog. If this dialog should be
* modal, the owner gets set to non modal.
* @param shouldBeModal - true if this should be modal, false otherwise.
* @param provider - the provider to use as the source of data.
*/
public IngestProgressSnapshotDialog(Container owner, Boolean shouldBeModal) {
public IngestProgressSnapshotDialog(Container owner, Boolean shouldBeModal, IngestProgressSnapshotProvider provider) {
super((Window) owner, TITLE, ModalityType.MODELESS);
if (shouldBeModal && owner instanceof JDialog) { // if called from a modal dialog, manipulate the parent be just under this in z order, and not modal.
final JDialog pseudoOwner = (JDialog) owner;
@@ -82,7 +84,7 @@ public final class IngestProgressSnapshotDialog extends JDialog {
this.getRootPane().registerKeyboardAction(e -> {
this.dispose();
}, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
add(new IngestProgressSnapshotPanel(this));
add(new IngestProgressSnapshotPanel(this, provider));
pack();
setResizable(false);
if (shouldBeModal) { // if called from a modal dialog, become modal, otherwise don't.
@@ -90,4 +92,17 @@ public final class IngestProgressSnapshotDialog extends JDialog {
}
setVisible(true);
}
/**
* Constructs an instance of the dialog with its own frame. Could be modal.
* Uses the internal IngestManager instance as the source of data for the
* dialog
*
* @param owner - the owner of this dialog. If this dialog should be
* modal, the owner gets set to non modal.
* @param shouldBeModal - true if this should be modal, false otherwise.
*/
public IngestProgressSnapshotDialog(Container owner, Boolean shouldBeModal) {
this(owner, shouldBeModal, IngestManager.getInstance());
}
}
@@ -33,15 +33,17 @@ import org.openide.util.NbBundle;
/**
* A panel that displays ingest task progress snapshots.
*/
public class IngestProgressSnapshotPanel extends javax.swing.JPanel {
class IngestProgressSnapshotPanel extends javax.swing.JPanel {
private final JDialog parent;
private final IngestProgressSnapshotProvider snapshotProvider;
private final IngestThreadActivitySnapshotsTableModel threadActivityTableModel;
private final IngestJobTableModel jobTableModel;
private final ModuleTableModel moduleTableModel;
IngestProgressSnapshotPanel(JDialog parent) {
IngestProgressSnapshotPanel(JDialog parent, IngestProgressSnapshotProvider snapshotProvider) {
this.parent = parent;
this.snapshotProvider = snapshotProvider;
threadActivityTableModel = new IngestThreadActivitySnapshotsTableModel();
jobTableModel = new IngestJobTableModel();
moduleTableModel = new ModuleTableModel();
@@ -105,7 +107,7 @@ public class IngestProgressSnapshotPanel extends javax.swing.JPanel {
}
private void refresh() {
snapshots = IngestManager.getInstance().getIngestThreadActivitySnapshots();
snapshots = snapshotProvider.getIngestThreadActivitySnapshots();
fireTableDataChanged();
}
@@ -187,7 +189,7 @@ public class IngestProgressSnapshotPanel extends javax.swing.JPanel {
}
private void refresh() {
jobSnapshots = IngestManager.getInstance().getIngestJobSnapshots();
jobSnapshots = snapshotProvider.getIngestJobSnapshots();
fireTableDataChanged();
}
@@ -299,7 +301,7 @@ public class IngestProgressSnapshotPanel extends javax.swing.JPanel {
}
private void refresh() {
Map<String, Long> moduleStatMap = IngestManager.getInstance().getModuleRunTimes();
Map<String, Long> moduleStatMap = snapshotProvider.getModuleRunTimes();
moduleStats.clear();
totalTime = 0;
for (String k : moduleStatMap.keySet()) {
@@ -0,0 +1,49 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2018 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.ingest;
import java.util.List;
import java.util.Map;
/**
* Interface that provides a snapshot of ingest progress.
*/
public interface IngestProgressSnapshotProvider {
/**
* Get a snapshot of the state of ingest threads.
*
* @return A list of IngestThreadActivitySnapshot
*/
List<IngestManager.IngestThreadActivitySnapshot> getIngestThreadActivitySnapshots();
/**
* Get a snapshot of the state of ingest jobs.
*
* @return A list of ingest job snapshots.
*/
List<DataSourceIngestJob.Snapshot> getIngestJobSnapshots();
/**
* Gets the cumulative run times for the ingest module.
*
* @return Map of module name to run time (in milliseconds)
*/
Map<String, Long> getModuleRunTimes();
}
@@ -18,6 +18,7 @@
*/
package org.sleuthkit.autopsy.ingest;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -555,7 +556,11 @@ final class IngestTasksScheduler {
* @return
*/
synchronized IngestJobTasksSnapshot getTasksSnapshotForJob(long jobId) {
return new IngestJobTasksSnapshot(jobId);
return new IngestJobTasksSnapshot(jobId, this.dataSourceIngestThreadQueue.countQueuedTasksForJob(jobId),
countTasksForJob(this.rootFileTaskQueue, jobId),
countTasksForJob(this.pendingFileTaskQueue, jobId),
this.fileIngestThreadsQueue.countQueuedTasksForJob(jobId),
this.dataSourceIngestThreadQueue.countRunningTasksForJob(jobId) + this.fileIngestThreadsQueue.countRunningTasksForJob(jobId));
}
/**
@@ -825,8 +830,9 @@ final class IngestTasksScheduler {
/**
* A snapshot of ingest tasks data for an ingest job.
*/
class IngestJobTasksSnapshot {
static final class IngestJobTasksSnapshot implements Serializable {
private static final long serialVersionUID = 1L;
private final long jobId;
private final long dsQueueSize;
private final long rootQueueSize;
@@ -839,13 +845,13 @@ final class IngestTasksScheduler {
*
* @param jobId The identifier associated with the job.
*/
IngestJobTasksSnapshot(long jobId) {
IngestJobTasksSnapshot(long jobId, long dsQueueSize, long rootQueueSize, long dirQueueSize, long fileQueueSize, long runningListSize) {
this.jobId = jobId;
this.dsQueueSize = IngestTasksScheduler.this.dataSourceIngestThreadQueue.countQueuedTasksForJob(jobId);
this.rootQueueSize = countTasksForJob(IngestTasksScheduler.this.rootFileTaskQueue, jobId);
this.dirQueueSize = countTasksForJob(IngestTasksScheduler.this.pendingFileTaskQueue, jobId);
this.fileQueueSize = IngestTasksScheduler.this.fileIngestThreadsQueue.countQueuedTasksForJob(jobId);;
this.runningListSize = IngestTasksScheduler.this.dataSourceIngestThreadQueue.countRunningTasksForJob(jobId) + IngestTasksScheduler.this.fileIngestThreadsQueue.countRunningTasksForJob(jobId);
this.dsQueueSize = dsQueueSize;
this.rootQueueSize = rootQueueSize;
this.dirQueueSize = dirQueueSize;
this.fileQueueSize = fileQueueSize;
this.runningListSize = runningListSize;
}
/**
@@ -9,8 +9,8 @@ OpenIDE-Module-Name=Embedded File Extraction
OpenIDE-Module-Short-Description=Embedded File Extraction Ingest Module
EmbeddedFileExtractorIngestModule.SevenZipContentReadStream.seek.exception.invalidOrigin=Invalid seek origin\: {0}
EmbeddedFileExtractorIngestModule.SevenZipContentReadStream.read.exception.errReadStream=Error reading content stream.
EmbeddedFileExtractorIngestModule.ArchiveExtractor.encryptionFileLevel=File-level Encryption
EmbeddedFileExtractorIngestModule.ArchiveExtractor.encryptionFull=Full Encryption
EmbeddedFileExtractorIngestModule.ArchiveExtractor.encryptionFileLevel=Content-only Encryption (Archive File)
EmbeddedFileExtractorIngestModule.ArchiveExtractor.encryptionFull=Full Encryption (Archive File)
EmbeddedFileExtractorIngestModule.ArchiveExtractor.init.errInitModule.details=Error initializing output dir\: {0}\: {1}
EmbeddedFileExtractorIngestModule.ArchiveExtractor.isZipBombCheck.warnMsg=Possible ZIP bomb detected in archive\: {0}, item\: {1}
EmbeddedFileExtractorIngestModule.ArchiveExtractor.isZipBombCheck.warnDetails=Compression ratio is {0}, skipping item in {1}.
@@ -65,6 +65,9 @@ final class EncryptionDetectionFileIngestModule extends FileIngestModuleAdapter
private static final int FILE_SIZE_MODULUS = 512;
private static final String DATABASE_FILE_EXTENSION = "db";
private static final int MINIMUM_DATABASE_FILE_SIZE = 65536; //64 KB
private static final String MIME_TYPE_OOXML_PROTECTED = "application/x-ooxml-protected";
private static final String MIME_TYPE_MSWORD = "application/msword";
private static final String MIME_TYPE_MSEXCEL = "application/vnd.ms-excel";
@@ -141,8 +144,7 @@ final class EncryptionDetectionFileIngestModule extends FileIngestModuleAdapter
}
} else {
if (isFilePasswordProtected(file)) {
return flagFile(file, BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_DETECTED,
Bundle.EncryptionDetectionFileIngestModule_artifactComment_password());
return flagFile(file, BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_DETECTED, Bundle.EncryptionDetectionFileIngestModule_artifactComment_password());
}
}
}
@@ -182,7 +184,6 @@ final class EncryptionDetectionFileIngestModule extends FileIngestModuleAdapter
private IngestModule.ProcessResult flagFile(AbstractFile file, BlackboardArtifact.ARTIFACT_TYPE artifactType, String comment) {
try {
BlackboardArtifact artifact = file.newArtifact(artifactType);
artifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT,
EncryptionDetectionModuleFactory.getModuleName(), comment));
@@ -362,18 +363,30 @@ final class EncryptionDetectionFileIngestModule extends FileIngestModuleAdapter
/*
* Qualify the size.
*/
boolean fileSizeQualified = false;
String fileExtension = file.getNameExtension();
long contentSize = file.getSize();
if (contentSize >= minimumFileSize) {
// Database files qualify at 64 KB minimum for SQLCipher detection.
if (fileExtension.equalsIgnoreCase(DATABASE_FILE_EXTENSION)) {
if (contentSize >= MINIMUM_DATABASE_FILE_SIZE) {
fileSizeQualified = true;
}
} else if (contentSize >= minimumFileSize) {
if (!fileSizeMultipleEnforced || (contentSize % FILE_SIZE_MODULUS) == 0) {
/*
* Qualify the entropy.
*/
calculatedEntropy = EncryptionDetectionTools.calculateEntropy(file);
if (calculatedEntropy >= minimumEntropy) {
possiblyEncrypted = true;
}
fileSizeQualified = true;
}
}
if (fileSizeQualified) {
/*
* Qualify the entropy.
*/
calculatedEntropy = EncryptionDetectionTools.calculateEntropy(file);
if (calculatedEntropy >= minimumEntropy) {
possiblyEncrypted = true;
}
}
return possiblyEncrypted;
}
}
@@ -18,8 +18,6 @@
*/
package org.sleuthkit.autopsy.modules.hashdatabase;
import java.util.ArrayList;
import java.util.List;
import org.openide.util.NbBundle;
import org.openide.util.lookup.ServiceProvider;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
@@ -0,0 +1,465 @@
/*
*
* Autopsy Forensic Browser
*
* Copyright 2018 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.commonfilessearch;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import junit.framework.Test;
import org.netbeans.junit.NbModuleSuite;
import org.netbeans.junit.NbTestCase;
import org.openide.util.Exceptions;
import org.python.icu.impl.Assert;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.commonfilesearch.AllDataSourcesCommonFilesAlgorithm;
import org.sleuthkit.autopsy.commonfilesearch.CommonFilesMetadata;
import org.sleuthkit.autopsy.commonfilesearch.CommonFilesMetadataBuilder;
import org.sleuthkit.autopsy.commonfilesearch.SingleDataSource;
import static org.sleuthkit.autopsy.commonfilessearch.IntraCaseUtils.*;
import org.sleuthkit.autopsy.ingest.IngestJobSettings;
import org.sleuthkit.autopsy.ingest.IngestJobSettings.IngestType;
import org.sleuthkit.autopsy.ingest.IngestModuleTemplate;
import org.sleuthkit.autopsy.modules.filetypeid.FileTypeIdModuleFactory;
import org.sleuthkit.autopsy.modules.hashdatabase.HashLookupModuleFactory;
import org.sleuthkit.autopsy.testutils.IngestUtils;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.TskCoreException;
/**
* Add set 1, set 2, set 3, and set 4 to case and ingest with hash algorithm.
*/
public class IngestedWithHashAndFileType extends NbTestCase {
public static Test suite() {
NbModuleSuite.Configuration conf = NbModuleSuite.createConfiguration(IngestedWithHashAndFileType.class).
clusters(".*").
enableModules(".*");
return conf.suite();
}
private final IntraCaseUtils utils;
public IngestedWithHashAndFileType(String name) {
super(name);
this.utils = new IntraCaseUtils(this, "IngestedWithHashAndFileTypeTests");
}
@Override
public void setUp() {
this.utils.setUp();
IngestModuleTemplate hashLookupTemplate = IngestUtils.getIngestModuleTemplate(new HashLookupModuleFactory());
IngestModuleTemplate mimeTypeLookupTemplate = IngestUtils.getIngestModuleTemplate(new FileTypeIdModuleFactory());
ArrayList<IngestModuleTemplate> templates = new ArrayList<>();
templates.add(hashLookupTemplate);
templates.add(mimeTypeLookupTemplate);
IngestJobSettings ingestJobSettings = new IngestJobSettings(IngestedWithHashAndFileType.class.getCanonicalName(), IngestType.FILES_ONLY, templates);
try {
IngestUtils.runIngestJob(Case.getCurrentCaseThrows().getDataSources(), ingestJobSettings);
} catch (NoCurrentCaseException | TskCoreException ex) {
Exceptions.printStackTrace(ex);
Assert.fail(ex);
}
}
@Override
public void tearDown() {
this.utils.tearDown();
}
/**
* Find all matches & all file types. Confirm file.jpg is found on all three
* and file.docx is found on two.
*/
public void testOneA() {
try {
Map<Long, String> dataSources = this.utils.getDataSourceMap();
CommonFilesMetadataBuilder allSourcesBuilder = new AllDataSourcesCommonFilesAlgorithm(dataSources, false, false);
CommonFilesMetadata metadata = allSourcesBuilder.findCommonFiles();
Map<Long, String> objectIdToDataSource = IntraCaseUtils.mapFileInstancesToDataSources(metadata);
List<AbstractFile> files = IntraCaseUtils.getFiles(objectIdToDataSource.keySet());
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET1, 2));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET2, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET3, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET1, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET3, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET1, 0));
assertTrue(IntraCaseUtils.verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET4, 0));
} catch (NoCurrentCaseException | TskCoreException | SQLException ex) {
Exceptions.printStackTrace(ex);
Assert.fail(ex);
}
}
/**
* Find all matches & only image types. Confirm file.jpg is found on all
* three.
*/
public void testOneB() {
try {
Map<Long, String> dataSources = this.utils.getDataSourceMap();
CommonFilesMetadataBuilder allSourcesBuilder = new AllDataSourcesCommonFilesAlgorithm(dataSources, true, false);
CommonFilesMetadata metadata = allSourcesBuilder.findCommonFiles();
Map<Long, String> objectIdToDataSource = mapFileInstancesToDataSources(metadata);
List<AbstractFile> files = getFiles(objectIdToDataSource.keySet());
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET1, 2));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET2, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET3, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET4, 0));
} catch (NoCurrentCaseException | TskCoreException | SQLException ex) {
Exceptions.printStackTrace(ex);
Assert.fail(ex);
}
}
/**
* Find all matches & only image types. Confirm file.jpg is found on all
* three.
*/
public void testOneC() {
try {
Map<Long, String> dataSources = this.utils.getDataSourceMap();
CommonFilesMetadataBuilder allSourcesBuilder = new AllDataSourcesCommonFilesAlgorithm(dataSources, false, true);
CommonFilesMetadata metadata = allSourcesBuilder.findCommonFiles();
Map<Long, String> objectIdToDataSource = mapFileInstancesToDataSources(metadata);
List<AbstractFile> files = getFiles(objectIdToDataSource.keySet());
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET1, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET3, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET4, 0));
} catch (NoCurrentCaseException | TskCoreException | SQLException ex) {
Exceptions.printStackTrace(ex);
Assert.fail(ex);
}
}
/**
* Find matches on set 1 & all file types. Confirm same results.
*
*/
public void testTwoA() {
try {
Map<Long, String> dataSources = this.utils.getDataSourceMap();
Long first = getDataSourceIdByName(SET1, dataSources);
CommonFilesMetadataBuilder singleSourceBuilder = new SingleDataSource(first, dataSources, false, false);
CommonFilesMetadata metadata = singleSourceBuilder.findCommonFiles();
Map<Long, String> objectIdToDataSource = mapFileInstancesToDataSources(metadata);
List<AbstractFile> files = getFiles(objectIdToDataSource.keySet());
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET1, 2));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET2, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET3, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET1, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET3, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET4, 0));
} catch (NoCurrentCaseException | TskCoreException | SQLException ex) {
Exceptions.printStackTrace(ex);
Assert.fail(ex);
}
}
/**
* Find matches on set 1 & only media types. Confirm same results.
*
*/
public void testTwoB() {
try {
Map<Long, String> dataSources = this.utils.getDataSourceMap();
Long first = getDataSourceIdByName(SET1, dataSources);
CommonFilesMetadataBuilder singleSourceBuilder = new SingleDataSource(first, dataSources, true, false);
CommonFilesMetadata metadata = singleSourceBuilder.findCommonFiles();
Map<Long, String> objectIdToDataSource = mapFileInstancesToDataSources(metadata);
List<AbstractFile> files = getFiles(objectIdToDataSource.keySet());
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET1, 2));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET2, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET3, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET4, 0));
} catch (NoCurrentCaseException | TskCoreException | SQLException ex) {
Exceptions.printStackTrace(ex);
Assert.fail(ex);
}
}
/**
* Find matches on set 1 & all file types. Confirm same results.
*
*/
public void testTwoC() {
try {
Map<Long, String> dataSources = this.utils.getDataSourceMap();
Long first = getDataSourceIdByName(SET1, dataSources);
CommonFilesMetadataBuilder singleSourceBuilder = new SingleDataSource(first, dataSources, false, true);
CommonFilesMetadata metadata = singleSourceBuilder.findCommonFiles();
Map<Long, String> objectIdToDataSource = mapFileInstancesToDataSources(metadata);
List<AbstractFile> files = getFiles(objectIdToDataSource.keySet());
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET1, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET3, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET4, 0));
} catch (NoCurrentCaseException | TskCoreException | SQLException ex) {
Exceptions.printStackTrace(ex);
Assert.fail(ex);
}
}
/**
* Find matches on set 2 & all file types: Confirm file.jpg.
*
*/
public void testThree() {
try {
Map<Long, String> dataSources = this.utils.getDataSourceMap();
Long second = getDataSourceIdByName(SET2, dataSources);
CommonFilesMetadataBuilder singleSourceBuilder = new SingleDataSource(second, dataSources, false, false);
CommonFilesMetadata metadata = singleSourceBuilder.findCommonFiles();
Map<Long, String> objectIdToDataSource = mapFileInstancesToDataSources(metadata);
List<AbstractFile> files = getFiles(objectIdToDataSource.keySet());
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET1, 2));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET2, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET3, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET4, 0));
} catch (NoCurrentCaseException | TskCoreException | SQLException ex) {
Exceptions.printStackTrace(ex);
Assert.fail(ex);
}
}
/**
* Find matches on set 4 & all file types: Confirm nothing is found.
*/
public void testFour() {
try {
Map<Long, String> dataSources = this.utils.getDataSourceMap();
Long last = getDataSourceIdByName(SET4, dataSources);
CommonFilesMetadataBuilder singleSourceBuilder = new SingleDataSource(last, dataSources, false, false);
CommonFilesMetadata metadata = singleSourceBuilder.findCommonFiles();
Map<Long, String> objectIdToDataSource = mapFileInstancesToDataSources(metadata);
List<AbstractFile> files = getFiles(objectIdToDataSource.keySet());
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET4, 0));
} catch (NoCurrentCaseException | TskCoreException | SQLException ex) {
Exceptions.printStackTrace(ex);
Assert.fail(ex);
}
}
/**
* Find matches on set 3 & all file types: Confirm file.jpg and file.docx.
*/
public void testFive() {
try {
Map<Long, String> dataSources = this.utils.getDataSourceMap();
Long third = getDataSourceIdByName(SET3, dataSources);
CommonFilesMetadataBuilder singleSourceBuilder = new SingleDataSource(third, dataSources, false, false);
CommonFilesMetadata metadata = singleSourceBuilder.findCommonFiles();
Map<Long, String> objectIdToDataSource = mapFileInstancesToDataSources(metadata);
List<AbstractFile> files = getFiles(objectIdToDataSource.keySet());
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET1, 2));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET2, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET3, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, IMG, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET1, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET3, 1));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, DOC, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, PDF, SET4, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET1, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET2, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET3, 0));
assertTrue(verifyFileExistanceAndCount(files, objectIdToDataSource, EMPTY, SET4, 0));
} catch (NoCurrentCaseException | TskCoreException | SQLException ex) {
Exceptions.printStackTrace(ex);
Assert.fail(ex);
}
}
}
@@ -0,0 +1,140 @@
/*
*
* Autopsy Forensic Browser
*
* Copyright 2018 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.commonfilessearch;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static junit.framework.Assert.assertTrue;
import junit.framework.Test;
import org.netbeans.junit.NbModuleSuite;
import org.netbeans.junit.NbTestCase;
import org.openide.util.Exceptions;
import org.python.icu.impl.Assert;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.commonfilesearch.AllDataSourcesCommonFilesAlgorithm;
import org.sleuthkit.autopsy.commonfilesearch.CommonFilesMetadata;
import org.sleuthkit.autopsy.commonfilesearch.CommonFilesMetadataBuilder;
import org.sleuthkit.autopsy.commonfilesearch.SingleDataSource;
import org.sleuthkit.autopsy.ingest.IngestJobSettings;
import org.sleuthkit.autopsy.ingest.IngestModuleTemplate;
import org.sleuthkit.autopsy.modules.hashdatabase.HashLookupModuleFactory;
import org.sleuthkit.autopsy.testutils.IngestUtils;
import static org.sleuthkit.autopsy.testutils.IngestUtils.getIngestModuleTemplate;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.TskCoreException;
/**
* Ingested w/o mime type info added to DB.
*
* Setup:
*
* Add images set 1, set 2, set 3, and set 4 to case. Do not run mime type
* module.
*/
public class IngestedWithNoFileTypes extends NbTestCase {
public static Test suite() {
NbModuleSuite.Configuration conf = NbModuleSuite.createConfiguration(IngestedWithNoFileTypes.class).
clusters(".*").
enableModules(".*");
return conf.suite();
}
private final IntraCaseUtils utils;
public IngestedWithNoFileTypes(String name) {
super(name);
this.utils = new IntraCaseUtils(this, "IngestedWithNoFileTypes");
}
@Override
public void setUp() {
this.utils.setUp();
IngestModuleTemplate hashLookupTemplate = getIngestModuleTemplate(new HashLookupModuleFactory());
ArrayList<IngestModuleTemplate> templates = new ArrayList<>();
templates.add(hashLookupTemplate);
IngestJobSettings ingestJobSettings = new IngestJobSettings(IngestedWithHashAndFileType.class.getCanonicalName(), IngestJobSettings.IngestType.FILES_ONLY, templates);
try {
IngestUtils.runIngestJob(Case.getCurrentCaseThrows().getDataSources(), ingestJobSettings);
} catch (NoCurrentCaseException | TskCoreException ex) {
Exceptions.printStackTrace(ex);
Assert.fail(ex);
}
}
@Override
public void tearDown(){
this.utils.tearDown();
}
/**
* Search using all data sources and filtering for media types. We should
* find nothing and no errors should arise.
*/
public void testOne() {
try {
Map<Long, String> dataSources = this.utils.getDataSourceMap();
CommonFilesMetadataBuilder allSourcesBuilder = new AllDataSourcesCommonFilesAlgorithm(dataSources, true, false);
CommonFilesMetadata metadata = allSourcesBuilder.findCommonFiles();
Map<Long, String> objectIdToDataSource = IntraCaseUtils.mapFileInstancesToDataSources(metadata);
List<AbstractFile> files = IntraCaseUtils.getFiles(objectIdToDataSource.keySet());
assertTrue(files.isEmpty());
} catch (NoCurrentCaseException | TskCoreException | SQLException ex) {
Exceptions.printStackTrace(ex);
}
}
/**
* Search using single data source and filtering for doc types. Observe that
* nothing is found and that nothing blows up.
*/
public void testTwo() {
try {
Map<Long, String> dataSources = this.utils.getDataSourceMap();
Long third = IntraCaseUtils.getDataSourceIdByName(IntraCaseUtils.SET3, dataSources);
CommonFilesMetadataBuilder singleSourceBuilder = new SingleDataSource(third, dataSources, true, false);
CommonFilesMetadata metadata = singleSourceBuilder.findCommonFiles();
Map<Long, String> objectIdToDataSource = IntraCaseUtils.mapFileInstancesToDataSources(metadata);
List<AbstractFile> files = IntraCaseUtils.getFiles(objectIdToDataSource.keySet());
assertTrue(files.isEmpty());
} catch (NoCurrentCaseException | TskCoreException | SQLException ex) {
Exceptions.printStackTrace(ex);
Assert.fail(ex);
}
}
}
@@ -0,0 +1,220 @@
/*
*
* Autopsy Forensic Browser
*
* Copyright 2018 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.commonfilessearch;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.netbeans.junit.NbTestCase;
import org.openide.util.Exceptions;
import org.python.icu.impl.Assert;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.ImageDSProcessor;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.commonfilesearch.CommonFilesMetadata;
import org.sleuthkit.autopsy.commonfilesearch.DataSourceLoader;
import org.sleuthkit.autopsy.commonfilesearch.FileInstanceMetadata;
import org.sleuthkit.autopsy.commonfilesearch.Md5Metadata;
import org.sleuthkit.autopsy.testutils.CaseUtils;
import org.sleuthkit.autopsy.testutils.IngestUtils;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.TskCoreException;
/**
*
* Provides setup and utility for testing presence of files in different data
* sets discoverable by Common Files Features.
*
* Data set definitions:
*
* set 1
* + file1
* - IMG_6175.jpg
* + file2
* - IMG_6175.jpg
* + file3
* - BasicStyleGuide.doc
*
* set 2
* - adsf.pdf
* - IMG_6175.jpg
*
* set 3
* - BasicStyleGuide.doc
* - IMG_6175.jpg
*
* set 4
* - file.dat (empty file)
*/
class IntraCaseUtils {
private static final String CASE_NAME = "IntraCaseCommonFilesSearchTest";
static final Path CASE_DIRECTORY_PATH = Paths.get(System.getProperty("java.io.tmpdir"), CASE_NAME);
private final Path imagePath1;
private final Path imagePath2;
private final Path imagePath3;
private final Path imagePath4;
static final String IMG = "IMG_6175.jpg";
static final String DOC = "BasicStyleGuide.doc";
static final String PDF = "adsf.pdf"; //not a typo - it appears this way in the test image
static final String EMPTY = "file.dat";
static final String SET1 = "commonfiles_image1_v1.vhd";
static final String SET2 = "commonfiles_image2_v1.vhd";
static final String SET3 = "commonfiles_image3_v1.vhd";
static final String SET4 = "commonfiles_image4_v1.vhd";
private final DataSourceLoader dataSourceLoader;
private final String caseName;
IntraCaseUtils(NbTestCase nbTestCase, String caseName){
imagePath1 = Paths.get(nbTestCase.getDataDir().toString(), "commonfiles_image1_v1.vhd");
imagePath2 = Paths.get(nbTestCase.getDataDir().toString(), "commonfiles_image2_v1.vhd");
imagePath3 = Paths.get(nbTestCase.getDataDir().toString(), "commonfiles_image3_v1.vhd");
imagePath4 = Paths.get(nbTestCase.getDataDir().toString(), "commonfiles_image4_v1.vhd");
this.dataSourceLoader = new DataSourceLoader();
this.caseName = caseName;
}
void setUp(){
CaseUtils.createAsCurrentCase(this.caseName);
final ImageDSProcessor imageDSProcessor = new ImageDSProcessor();
IngestUtils.addDataSource(imageDSProcessor, imagePath1);
IngestUtils.addDataSource(imageDSProcessor, imagePath2);
IngestUtils.addDataSource(imageDSProcessor, imagePath3);
IngestUtils.addDataSource(imageDSProcessor, imagePath4);
}
Map<Long, String> getDataSourceMap() throws NoCurrentCaseException, TskCoreException, SQLException{
return this.dataSourceLoader.getDataSourceMap();
}
void tearDown(){
CaseUtils.closeCurrentCase(false);
try {
CaseUtils.deleteCaseDir(CASE_DIRECTORY_PATH.toFile());
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
//does not represent a failure in the common files search feature
}
}
/**
* Verify that the given file appears a precise number times in the given
* data source.
*
* @param files search domain
* @param objectIdToDataSource mapping of file ids to data source names
* @param name name of file to search for
* @param dataSource name of data source where file should appear
* @param count number of appearances of the given file
* @return true if a file with the given name exists the specified number
* of times in the given data source
*/
static boolean verifyFileExistanceAndCount(List<AbstractFile> files, Map<Long, String> objectIdToDataSource, String name, String dataSource, int count) {
int tally = 0;
for (AbstractFile file : files) {
Long objectId = file.getId();
String fileName = file.getName();
String dataSourceName = objectIdToDataSource.get(objectId);
if (fileName.equals(name) && dataSourceName.equals(dataSource)) {
tally++;
}
}
return tally == count;
}
/**
* Convenience method which verifies that a file exists within a given data
* source exactly once.
*
* @param files search domain
* @param objectIdToDataSource mapping of file ids to data source names
* @param name name of file to search for
* @param dataSource name of data source where file should appear
* @return true if a file with the given name exists once in the given data
* source
*/
static boolean verifySingularFileExistance(List<AbstractFile> files, Map<Long, String> objectIdToDataSource, String name, String dataSource) {
return verifyFileExistanceAndCount(files, objectIdToDataSource, name, dataSource, 1);
}
static Map<Long, String> mapFileInstancesToDataSources(CommonFilesMetadata metadata) {
Map<Long, String> instanceIdToDataSource = new HashMap<>();
for (Map.Entry<String, Md5Metadata> entry : metadata.getMetadata().entrySet()) {
for (FileInstanceMetadata md : entry.getValue().getMetadata()) {
instanceIdToDataSource.put(md.getObjectId(), md.getDataSourceName());
}
}
return instanceIdToDataSource;
}
static List<AbstractFile> getFiles(Set<Long> objectIds) {
List<AbstractFile> files = new ArrayList<>(objectIds.size());
for (Long id : objectIds) {
try {
AbstractFile file = Case.getCurrentCaseThrows().getSleuthkitCase().getAbstractFileById(id);
files.add(file);
} catch (NoCurrentCaseException | TskCoreException ex) {
Exceptions.printStackTrace(ex);
Assert.fail(ex);
}
}
return files;
}
static Long getDataSourceIdByName(String name, Map<Long, String> dataSources){
if(dataSources.containsValue(name)){
for(Map.Entry<Long, String> dataSource : dataSources.entrySet()){
if(dataSource.getValue().equals(name)){
return dataSource.getKey();
}
}
} else {
throw new IndexOutOfBoundsException(String.format("Name should be one of: {0}", String.join(",", dataSources.values())));
}
return null;
}
}
@@ -0,0 +1,114 @@
/*
*
* Autopsy Forensic Browser
*
* Copyright 2018 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.commonfilessearch;
import java.sql.SQLException;
import java.util.Map;
import static junit.framework.Assert.assertEquals;
import junit.framework.Test;
import org.netbeans.junit.NbModuleSuite;
import org.netbeans.junit.NbTestCase;
import org.openide.util.Exceptions;
import org.python.icu.impl.Assert;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.commonfilesearch.AllDataSourcesCommonFilesAlgorithm;
import org.sleuthkit.autopsy.commonfilesearch.CommonFilesMetadata;
import org.sleuthkit.autopsy.commonfilesearch.CommonFilesMetadataBuilder;
import org.sleuthkit.autopsy.commonfilesearch.SingleDataSource;
import static org.sleuthkit.autopsy.commonfilessearch.IntraCaseUtils.SET1;
import static org.sleuthkit.autopsy.commonfilessearch.IntraCaseUtils.getDataSourceIdByName;
import org.sleuthkit.datamodel.TskCoreException;
/**
* Test that cases which are created but have not run any ingest modules turn up
* no results.
*
* Setup:
*
* Add images set 1, set 2, set 3, and set 4 to case. Do not ingest.
*
*/
public class UningestedCases extends NbTestCase {
public static Test suite() {
NbModuleSuite.Configuration conf = NbModuleSuite.createConfiguration(UningestedCases.class).
clusters(".*").
enableModules(".*");
return conf.suite();
}
private final IntraCaseUtils utils;
public UningestedCases(String name) {
super(name);
this.utils = new IntraCaseUtils(this, "UningestedCasesTests");
}
@Override
public void setUp(){
this.utils.setUp();
}
@Override
public void tearDown(){
this.utils.tearDown();
}
/**
* Find all matches & all file types. Confirm no matches are found (since
* there are no hashes to match).
*/
public void testOne() {
try {
Map<Long, String> dataSources = this.utils.getDataSourceMap();
CommonFilesMetadataBuilder allSourcesBuilder = new AllDataSourcesCommonFilesAlgorithm(dataSources, false, false);
CommonFilesMetadata metadata = allSourcesBuilder.findCommonFiles();
int resultCount = metadata.size();
assertEquals(resultCount, 0);
} catch (NoCurrentCaseException | TskCoreException | SQLException ex) {
Exceptions.printStackTrace(ex);
Assert.fail(ex);
}
}
/**
* Find all matches on image #1 & all file types. Confirm no matches.
*/
public void testTwo() {
try {
Map<Long, String> dataSources = this.utils.getDataSourceMap();
Long first = getDataSourceIdByName(SET1, dataSources);
CommonFilesMetadataBuilder singleSourceBuilder = new SingleDataSource(first, dataSources, false, false);
CommonFilesMetadata metadata = singleSourceBuilder.findCommonFiles();
int resultCount = metadata.size();
assertEquals(resultCount, 0);
} catch (NoCurrentCaseException | TskCoreException | SQLException ex) {
Exceptions.printStackTrace(ex);
Assert.fail(ex);
}
}
}
@@ -52,10 +52,12 @@ public class EncryptionDetectionTest extends NbTestCase {
private static final String BITLOCKER_DETECTION_CASE_NAME = "testBitlockerEncryption";
private static final String PASSWORD_DETECTION_CASE_NAME = "PasswordDetectionTest";
private static final String VERACRYPT_DETECTION_CASE_NAME = "VeraCryptDetectionTest";
private static final String SQLCIPHER_DETECTION_CASE_NAME = "SQLCipherDetectionTest";
private final Path BITLOCKER_DETECTION_IMAGE_PATH = Paths.get(this.getDataDir().toString(), "encryption_detection_bitlocker_test.vhd");
private final Path PASSWORD_DETECTION_IMAGE_PATH = Paths.get(this.getDataDir().toString(), "password_detection_test.img");
private final Path VERACRYPT_DETECTION_IMAGE_PATH = Paths.get(this.getDataDir().toString(), "veracrypt_detection_test.vhd");
private final Path SQLCIPHER_DETECTION_IMAGE_PATH = Paths.get(this.getDataDir().toString(), "encryption_detection_sqlcipher_test.vhd");
private boolean testSucceeded;
@@ -79,7 +81,7 @@ public class EncryptionDetectionTest extends NbTestCase {
public void tearDown() {
CaseUtils.closeCurrentCase(testSucceeded);
}
/**
* Test the Encryption Detection module's volume encryption detection.
*/
@@ -88,9 +90,9 @@ public class EncryptionDetectionTest extends NbTestCase {
Case openCase = CaseUtils.createAsCurrentCase(BITLOCKER_DETECTION_CASE_NAME);
ImageDSProcessor dataSourceProcessor = new ImageDSProcessor();
IngestUtils.addDataSource(dataSourceProcessor, BITLOCKER_DETECTION_IMAGE_PATH);
/*
* Create ingest job settings.
* Create ingest job settings and run ingest job.
*/
IngestModuleFactory ingestModuleFactory = new EncryptionDetectionModuleFactory();
IngestModuleIngestJobSettings settings = ingestModuleFactory.getDefaultIngestJobSettings();
@@ -164,7 +166,6 @@ public class EncryptionDetectionTest extends NbTestCase {
/*
* Create ingest job settings.
*/
ArrayList<IngestModuleTemplate> templates = new ArrayList<>();
templates.add(IngestUtils.getIngestModuleTemplate(new EncryptionDetectionModuleFactory()));
IngestJobSettings ingestJobSettings = new IngestJobSettings(PASSWORD_DETECTION_CASE_NAME, IngestType.FILES_ONLY, templates);
@@ -295,4 +296,67 @@ public class EncryptionDetectionTest extends NbTestCase {
testSucceeded = true;
}
/**
* Test the Encryption Detection module's SQLCipher encryption detection.
*/
public void testSqlCipherEncryption() {
try {
Case openCase = CaseUtils.createAsCurrentCase(SQLCIPHER_DETECTION_CASE_NAME);
ImageDSProcessor dataSourceProcessor = new ImageDSProcessor();
IngestUtils.addDataSource(dataSourceProcessor, SQLCIPHER_DETECTION_IMAGE_PATH);
/*
* Create ingest job settings.
*/
ArrayList<IngestModuleTemplate> templates = new ArrayList<>();
templates.add(IngestUtils.getIngestModuleTemplate(new EncryptionDetectionModuleFactory()));
IngestJobSettings ingestJobSettings = new IngestJobSettings(SQLCIPHER_DETECTION_CASE_NAME, IngestType.FILES_ONLY, templates);
IngestUtils.runIngestJob(openCase.getDataSources(), ingestJobSettings);
/*
* Purge specific files to be tested.
*/
FileManager fileManager = openCase.getServices().getFileManager();
List<AbstractFile> results = fileManager.findFiles("%%", "sqlcipher");
assertEquals("Unexpected number of SQLCipher results.", 15, results.size());
for (AbstractFile file : results) {
/*
* Process only non-slack files.
*/
if (file.isFile() && !file.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.SLACK)) {
/*
* Determine which assertions to use for the file based on
* its name.
*/
List<BlackboardArtifact> artifactsList = file.getAllArtifacts();
String[] splitNameArray = file.getName().split("\\.");
if (splitNameArray[0].startsWith("sqlcipher-") && splitNameArray[splitNameArray.length - 1].equals("db")) {
/*
* Check that the SQLCipher database file has one
* TSK_ENCRYPTION_SUSPECTED artifact.
*/
int artifactsListSize = artifactsList.size();
String errorMessage = String.format("File '%s' (objId=%d) has %d artifacts, but 1 was expected.", file.getName(), file.getId(), artifactsListSize);
assertEquals(errorMessage, 1, artifactsListSize);
String artifactTypeName = artifactsList.get(0).getArtifactTypeName();
errorMessage = String.format("File '%s' (objId=%d) has an unexpected '%s' artifact.", file.getName(), file.getId(), artifactTypeName);
assertEquals(errorMessage, BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_SUSPECTED.toString(), artifactTypeName);
} else {
/*
* Check that the file has no artifacts.
*/
int artifactsListSize = artifactsList.size();
String errorMessage = String.format("File '%s' (objId=%d) has %d artifacts, but none were expected.", file.getName(), file.getId(), artifactsListSize);
assertEquals(errorMessage, 0, artifactsListSize);
}
}
}
} catch (TskCoreException ex) {
Exceptions.printStackTrace(ex);
Assert.fail(ex);
}
}
}
@@ -38,8 +38,6 @@ import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
*/
public final class CaseUtils {
private static final String PRESERVE_CASE_DATA_LIST_FILE_NAME = ".preserve";
/**
* Create a case case directory and case for the given case name.
*
@@ -127,5 +125,4 @@ public final class CaseUtils {
*/
private CaseUtils() {
}
}