diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..90b51996f9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +language: java +sudo: required +dist: trusty +os: + - linux +env: + global: + - TSK_HOME=$TRAVIS_BUILD_DIR/sleuthkit/sleuthkit +jdk: + - oraclejdk8 +before_install: + - git clone https://github.com/sleuthkit/sleuthkit.git sleuthkit/sleuthkit +install: + - sudo apt-get install testdisk + - cd sleuthkit/sleuthkit + - sh install-sleuthkit.sh +script: + - cd $TRAVIS_BUILD_DIR/ + - ant build + - cd Core/ + - xvfb-run ant test + diff --git a/Core/build.xml b/Core/build.xml index c7669a0cd7..3555ff0711 100644 --- a/Core/build.xml +++ b/Core/build.xml @@ -12,7 +12,7 @@ - + @@ -83,6 +83,7 @@ + diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/AbstractSqlEamDb.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/AbstractSqlEamDb.java index b929956b1c..37cb78a51e 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/AbstractSqlEamDb.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/AbstractSqlEamDb.java @@ -176,9 +176,15 @@ public abstract class AbstractSqlEamDb implements EamDb { * @returns New Case class with populated database ID */ @Override - public CorrelationCase newCase(CorrelationCase eamCase) throws EamDbException { - Connection conn = connect(); + public synchronized CorrelationCase newCase(CorrelationCase eamCase) throws EamDbException { + + // check if there is already an existing CorrelationCase for this Case + CorrelationCase cRCase = getCaseByUUID(eamCase.getCaseUUID()); + if (cRCase != null) { + return cRCase; + } + Connection conn = connect(); PreparedStatement preparedStatement = null; String sql = "INSERT INTO cases(case_uid, org_id, case_name, creation_date, case_number, " diff --git a/Core/src/org/sleuthkit/autopsy/communications/AbstractCVTAction.java b/Core/src/org/sleuthkit/autopsy/communications/AbstractCVTAction.java new file mode 100644 index 0000000000..ef751d262c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/AbstractCVTAction.java @@ -0,0 +1,65 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.communications; + +import java.util.Collection; +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import javax.swing.JMenuItem; +import org.openide.util.Utilities; +import org.openide.util.actions.Presenter; + +/** + * Base class for actions that act on the selected AccountDeviceInstanceKeys. + * getPopupPresenter() provides a JMenuItem that works (i.e., has an icon) in + * custom context menus and also in the Netbeans Explorer views. + */ +abstract class AbstractCVTAction extends AbstractAction implements Presenter.Popup { + + /** + * Get the selected accounts that will be acted upon. + * + * @return The selected accounts + */ + Collection getSelectedAccounts() { + return Utilities.actionsGlobalContext().lookupAll(AccountDeviceInstanceKey.class); + } + + @Override + public JMenuItem getPopupPresenter() { + JMenuItem presenter = new JMenuItem(this); + presenter.setText(getActionDisplayName()); + presenter.setIcon(getIcon()); + return presenter; + } + + /** + * The name/text of the action as displayed in a menu. + * + * @return The diaplay name of this action + */ + abstract String getActionDisplayName(); + + /** + * The icon to use for this icon. + * + * @return An ImageIcon used to represent this action. + */ + abstract ImageIcon getIcon(); +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java b/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java index 81bb781771..044cec322f 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java +++ b/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java @@ -18,17 +18,13 @@ */ package org.sleuthkit.autopsy.communications; -import java.awt.event.ActionEvent; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import javax.swing.AbstractAction; import javax.swing.Action; import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; -import org.openide.util.Utilities; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.datamodel.Account; @@ -102,58 +98,8 @@ final class AccountDeviceInstanceNode extends AbstractNode { @Override public Action[] getActions(boolean context) { ArrayList actions = new ArrayList<>(Arrays.asList(super.getActions(context))); -// actions.add(PinAccountsAction.getInstance()); -// actions.add(ResetAndPinAccountsAction.getInstance()); + actions.add(PinAccountsAction.getInstance()); + actions.add(ResetAndPinAccountsAction.getInstance()); return actions.toArray(new Action[actions.size()]); } - - /** - * Action that pins the selected AccountDeviceInstances to the - * visualization. - */ - static private class PinAccountsAction extends AbstractAction { - - private static final long serialVersionUID = 1L; - private final static PinAccountsAction instance = new PinAccountsAction(); - - private static PinAccountsAction getInstance() { - return instance; - } - - private PinAccountsAction() { - super("Add Account(s) to Visualization"); - } - - @Override - public void actionPerformed(ActionEvent e) { - Collection lookupAll = - Utilities.actionsGlobalContext().lookupAll(AccountDeviceInstanceKey.class); - CVTEvents.getCVTEventBus().post(new CVTEvents.PinAccountsEvent(lookupAll, false)); - } - } - - /** - * Action that pins the selected AccountDeviceInstances to the - * visualization. - */ - static private class ResetAndPinAccountsAction extends AbstractAction { - - private static final long serialVersionUID = 1L; - private final static ResetAndPinAccountsAction instance = new ResetAndPinAccountsAction(); - - private static ResetAndPinAccountsAction getInstance() { - return instance; - } - - private ResetAndPinAccountsAction() { - super("Visualize Account(s)"); - } - - @Override - public void actionPerformed(ActionEvent e) { - Collection lookupAll = - Utilities.actionsGlobalContext().lookupAll(AccountDeviceInstanceKey.class); - CVTEvents.getCVTEventBus().post(new CVTEvents.PinAccountsEvent(lookupAll, true)); - } - } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties index 559e9bba86..358971eaa8 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties @@ -20,9 +20,6 @@ CVTTopComponent.vizPanel.TabConstraints.tabTitle=Visualize CVTTopComponent.accountsBrowser.TabConstraints.tabTitle_1=Browse CVTTopComponent.browseVisualizeTabPane.AccessibleContext.accessibleName=Visualize CVTTopComponent.vizPanel.TabConstraints.tabTitle_1=Visualize -VisualizationPanel.jButton6.text=Hierarchy -VisualizationPanel.jButton7.text=Circle -VisualizationPanel.jButton8.text=Organic VisualizationPanel.fitGraphButton.text= VisualizationPanel.jTextArea1.text=Right-click an account in the Browse Accounts table, and select 'Visualize' to begin. VisualizationPanel.jLabel1.text=Layouts: @@ -36,11 +33,8 @@ VisualizationPanel.zoomInButton.toolTipText=Zoom in VisualizationPanel.zoomInButton.text= VisualizationPanel.zoomOutButton.toolTipText=Zoom out VisualizationPanel.zoomOutButton.text= -# To change this license header, choose License Headers in Project Properties. -# To change this template file, choose Tools | Templates -# and open the template in the editor. VisualizationPanel.circleLayoutButton.text=Circle VisualizationPanel.organicLayoutButton.text=Organic VisualizationPanel.fastOrganicLayoutButton.text=Fast Organic -VisualizationPanel.hierarchyLayoutButton.text=Hierarchy +VisualizationPanel.hierarchyLayoutButton.text=Hierarchical VisualizationPanel.clearVizButton.text_1=Clear Viz. diff --git a/Core/src/org/sleuthkit/autopsy/communications/CVTEvents.java b/Core/src/org/sleuthkit/autopsy/communications/CVTEvents.java index e0f945bb69..6a6a60d3da 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/CVTEvents.java +++ b/Core/src/org/sleuthkit/autopsy/communications/CVTEvents.java @@ -21,7 +21,6 @@ package org.sleuthkit.autopsy.communications; import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.EventBus; import java.util.Collection; -import java.util.Set; import org.sleuthkit.datamodel.CommunicationsFilter; /** @@ -79,7 +78,7 @@ final class CVTEvents { return accountDeviceInstances; } - public UnpinAccountsEvent(Set accountDeviceInstances) { + UnpinAccountsEvent(Collection accountDeviceInstances) { this.accountDeviceInstances = ImmutableSet.copyOf(accountDeviceInstances); } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.form b/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.form index 89e2ae4905..055719c488 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.form +++ b/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.form @@ -1,10 +1,6 @@
- - - - @@ -72,6 +68,20 @@ + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.java b/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.java index a33816b3fd..09052d9d9e 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.java @@ -71,7 +71,7 @@ public final class CVTTopComponent extends TopComponent { * via an Eventbus */ CVTEvents.getCVTEventBus().register(this); -// CVTEvents.getCVTEventBus().register(vizPanel); + CVTEvents.getCVTEventBus().register(vizPanel); CVTEvents.getCVTEventBus().register(accountsBrowser); } @@ -88,13 +88,14 @@ public final class CVTTopComponent extends TopComponent { // //GEN-BEGIN:initComponents private void initComponents() { - vizPanel = new VisualizationPanel(); browseVisualizeTabPane = new JTabbedPane(); accountsBrowser = new AccountsBrowser(); + vizPanel = new VisualizationPanel(); filtersPane = new FiltersPanel(); browseVisualizeTabPane.setFont(new Font("Tahoma", 0, 18)); // NOI18N browseVisualizeTabPane.addTab(NbBundle.getMessage(CVTTopComponent.class, "CVTTopComponent.accountsBrowser.TabConstraints.tabTitle_1"), new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/table.png")), accountsBrowser); // NOI18N + browseVisualizeTabPane.addTab(NbBundle.getMessage(CVTTopComponent.class, "CVTTopComponent.vizPanel.TabConstraints.tabTitle_1"), new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/emblem-web.png")), vizPanel); // NOI18N filtersPane.setMinimumSize(new Dimension(256, 495)); @@ -105,7 +106,7 @@ public final class CVTTopComponent extends TopComponent { .addGap(6, 6, 6) .addComponent(filtersPane, GroupLayout.PREFERRED_SIZE, 265, GroupLayout.PREFERRED_SIZE) .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) - .addComponent(browseVisualizeTabPane, GroupLayout.DEFAULT_SIZE, 786, Short.MAX_VALUE) + .addComponent(browseVisualizeTabPane, GroupLayout.PREFERRED_SIZE, 786, Short.MAX_VALUE) .addContainerGap()) ); layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) diff --git a/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java b/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java index 49c3627e08..716d00656c 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java +++ b/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java @@ -25,7 +25,6 @@ import com.google.common.collect.MultimapBuilder; import com.mxgraph.model.mxCell; import com.mxgraph.model.mxICell; import com.mxgraph.util.mxConstants; -import com.mxgraph.view.mxCellState; import com.mxgraph.view.mxGraph; import com.mxgraph.view.mxStylesheet; import java.io.InputStream; @@ -84,17 +83,19 @@ final class CommunicationsGraph extends mxGraph { mxStylesheet.getDefaultEdgeStyle().put(mxConstants.STYLE_STARTARROW, mxConstants.NONE); } - /* Map from type specific account identifier to mxCell(vertex). */ + /** Map from type specific account identifier to mxCell(vertex). */ private final Map nodeMap = new HashMap<>(); - /* Map from relationship source (Content) to mxCell (edge). */ + /** Map from relationship source (Content) to mxCell (edge). */ private final Multimap edgeMap = MultimapBuilder.hashKeys().hashSetValues().build(); private final LockedVertexModel lockedVertexModel; private final PinnedAccountModel pinnedAccountModel; - CommunicationsGraph() { + CommunicationsGraph(PinnedAccountModel pinnedAccountModel, LockedVertexModel lockedVertexModel) { super(mxStylesheet); + this.pinnedAccountModel = pinnedAccountModel; + this.lockedVertexModel = lockedVertexModel; //set fixed properties of graph. setAutoSizeCells(true); setCellsCloneable(false); @@ -113,21 +114,6 @@ final class CommunicationsGraph extends mxGraph { setKeepEdgesInBackground(true); setResetEdgesOnMove(true); setHtmlLabels(true); - - lockedVertexModel = new LockedVertexModel(); - lockedVertexModel.registerhandler((LockedVertexModel.VertexLockEvent event) -> { - if (event.isVertexLocked()) { - getView().clear(event.getVertex(), true, true); - getView().validate(); - } else { - final mxCellState state = getView().getState(event.getVertex(), true); - getView().updateLabel(state); - getView().updateLabelBounds(state); - getView().updateBoundingBox(state); - } - }); - - pinnedAccountModel = new PinnedAccountModel(this); } /** @@ -255,20 +241,20 @@ final class CommunicationsGraph extends mxGraph { */ private class RebuildWorker extends SwingWorker { - private final ProgressIndicator progress; + private final ProgressIndicator progressIndicator; private final CommunicationsManager commsManager; private final CommunicationsFilter currentFilter; RebuildWorker(ProgressIndicator progress, CommunicationsManager commsManager, CommunicationsFilter currentFilter) { - this.progress = progress; + this.progressIndicator = progress; this.currentFilter = currentFilter; this.commsManager = commsManager; } @Override - protected Void doInBackground() throws Exception { - progress.start("Loading accounts"); + protected Void doInBackground() { + progressIndicator.start("Loading accounts"); int progressCounter = 0; try { /** @@ -279,18 +265,18 @@ final class CommunicationsGraph extends mxGraph { if (isCancelled()) { break; } - final List relatedAccountDeviceInstances = - commsManager.getRelatedAccountDeviceInstances(adiKey.getAccountDeviceInstance(), currentFilter); + //get accounts related to pinned account + final List relatedAccountDeviceInstances + = commsManager.getRelatedAccountDeviceInstances(adiKey.getAccountDeviceInstance(), currentFilter); relatedAccounts.put(adiKey.getAccountDeviceInstance(), adiKey); getOrCreateVertex(adiKey); - //get accounts related to pinned account for (final AccountDeviceInstance relatedADI : relatedAccountDeviceInstances) { final long adiRelationshipsCount = commsManager.getRelationshipSourcesCount(relatedADI, currentFilter); final AccountDeviceInstanceKey relatedADIKey = new AccountDeviceInstanceKey(relatedADI, currentFilter, adiRelationshipsCount); relatedAccounts.put(relatedADI, relatedADIKey); //store related accounts } - progress.progress(++progressCounter); + progressIndicator.progress(++progressCounter); } Set accounts = relatedAccounts.keySet(); @@ -298,19 +284,24 @@ final class CommunicationsGraph extends mxGraph { Map relationshipCounts = commsManager.getRelationshipCountsPairwise(accounts, currentFilter); int total = relationshipCounts.size(); - int k = 0; - progress.switchToDeterminate("", 0, total); + int progress = 0; + String progressText = ""; + progressIndicator.switchToDeterminate("", 0, total); for (Map.Entry entry : relationshipCounts.entrySet()) { Long count = entry.getValue(); AccountPair relationshipKey = entry.getKey(); AccountDeviceInstanceKey account1 = relatedAccounts.get(relationshipKey.getFirst()); AccountDeviceInstanceKey account2 = relatedAccounts.get(relationshipKey.getSecond()); - mxCell addEdge = addOrUpdateEdge(count, account1, account2); - progress.progress(addEdge.getId(), k++); + + if (pinnedAccountModel.isAccountPinned(account1) + || pinnedAccountModel.isAccountPinned(account2)) { + mxCell addEdge = addOrUpdateEdge(count, account1, account2); + progressText = addEdge.getId(); + } + progressIndicator.progress(progressText, progress++); } } catch (TskCoreException tskCoreException) { logger.log(Level.SEVERE, "Error", tskCoreException); - } finally { } return null; @@ -326,7 +317,7 @@ final class CommunicationsGraph extends mxGraph { } catch (CancellationException ex) { logger.log(Level.INFO, "Graph visualization cancelled"); } finally { - progress.finish(); + progressIndicator.finish(); } } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/EventHandler.java b/Core/src/org/sleuthkit/autopsy/communications/EventHandler.java deleted file mode 100644 index f1acedf1cf..0000000000 --- a/Core/src/org/sleuthkit/autopsy/communications/EventHandler.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2018 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.sleuthkit.autopsy.communications; - -/** - * - */ -public interface EventHandler { - - void handle(T event); -} diff --git a/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java b/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java index f26e9515b3..7d8b8c083d 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java @@ -18,20 +18,19 @@ */ package org.sleuthkit.autopsy.communications; +import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.EventBus; import com.mxgraph.model.mxCell; +import java.util.Collection; import java.util.HashSet; import java.util.Set; -class LockedVertexModel { - - void registerhandler(EventHandler handler) { - eventBus.register(handler); - } - - void unregisterhandler(EventHandler handler) { - eventBus.unregister(handler); - } +/** + * Model of which vertices in a graph are locked ( not moveable by layout + * algorithms). + * + */ +final class LockedVertexModel { private final EventBus eventBus = new EventBus(); @@ -42,30 +41,34 @@ class LockedVertexModel { */ private final Set lockedVertices = new HashSet<>(); - LockedVertexModel() { + void registerhandler(Object handler) { + eventBus.register(handler); + } + + void unregisterhandler(Object handler) { + eventBus.unregister(handler); } /** - * Lock the given vertex so that applying a layout algorithm doesn't move - * it. The user can still manually position the vertex. + * Lock the given vertices so that applying a layout algorithm doesn't move + * them. The user can still manually position the vertices. * * @param vertex The vertex to lock. */ - void lockVertex(mxCell vertex) { - lockedVertices.add(vertex); - eventBus.post(new VertexLockEvent(vertex, true)); - + void lock(Collection vertices) { + lockedVertices.addAll(vertices); + eventBus.post(new VertexLockEvent(true, vertices)); } /** - * Lock the given vertex so that applying a layout algorithm can move it. + * Unlock the given vertices so that applying a layout algorithm can move + * them. * * @param vertex The vertex to unlock. */ - void unlockVertex(mxCell vertex) { - lockedVertices.remove(vertex); - eventBus.post(new VertexLockEvent(vertex, false)); - + void unlock(Collection vertices) { + lockedVertices.removeAll(vertices); + eventBus.post(new VertexLockEvent(false, vertices)); } boolean isVertexLocked(mxCell vertex) { @@ -77,21 +80,36 @@ class LockedVertexModel { lockedVertices.clear(); } - static class VertexLockEvent { + boolean isEmpty() { + return lockedVertices.isEmpty(); + } - private final mxCell vertex; + /** + * Event that represents a change in the locked state of one or more + * vertices. + */ + final static class VertexLockEvent { - public mxCell getVertex() { - return vertex; + private final boolean locked; + private final Set vertices; + + /** + * @return The vertices whose locked state has changed. + */ + public Set getVertices() { + return vertices; } - public boolean isVertexLocked() { + /** + * @return True if the vertices are locked, False if the vertices are + * unlocked. + */ + public boolean isLocked() { return locked; } - private final boolean locked; - VertexLockEvent(mxCell vertex, boolean locked) { - this.vertex = vertex; + VertexLockEvent(boolean locked, Collection< mxCell> vertices) { + this.vertices = ImmutableSet.copyOf(vertices); this.locked = locked; } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java b/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java index 68309f5195..7b90357993 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java +++ b/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java @@ -35,7 +35,6 @@ import org.sleuthkit.autopsy.corecomponents.DataResultPanel; import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; import org.sleuthkit.autopsy.corecomponents.TableFilterNode; import org.sleuthkit.autopsy.directorytree.DataResultFilterNode; -import org.sleuthkit.datamodel.AccountDeviceInstance; /** * The right hand side of the CVT. Has a DataResultPanel to show a listing of @@ -151,10 +150,10 @@ public final class MessageBrowser extends JPanel implements ExplorerManager.Prov //Use lookup here? final AccountDeviceInstanceNode adiNode = (AccountDeviceInstanceNode) selectedNodes[0]; - final Set accountDeviceInstances = new HashSet<>(); + final Set accountDeviceInstances = new HashSet<>(); for (final Node n : selectedNodes) { //Use lookup here? - accountDeviceInstances.add(((AccountDeviceInstanceNode) n).getAccountDeviceInstance()); + accountDeviceInstances.add(((AccountDeviceInstanceNode) n).getAccountDeviceInstanceKey()); } return SelectionNode.createFromAccounts(accountDeviceInstances, adiNode.getFilter(), adiNode.getCommsManager()); } diff --git a/Core/src/org/sleuthkit/autopsy/communications/PinAccountsAction.java b/Core/src/org/sleuthkit/autopsy/communications/PinAccountsAction.java new file mode 100644 index 0000000000..52136d1b1a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/PinAccountsAction.java @@ -0,0 +1,59 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.communications; + +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import org.openide.util.ImageUtilities; +import org.openide.util.NbBundle; + +/** + * Action that pins the AccountDevicesIntanceKeys in the ActionsGlobalContext to + * the visualizaion + */ +@NbBundle.Messages({"PinAccountsAction.pluralText=Add Selected Accounts to Visualization", + "PinAccountsAction.singularText=Add Selected Account to Visualization"}) +final class PinAccountsAction extends AbstractCVTAction { + + static private final ImageIcon ICON = ImageUtilities.loadImageIcon( + "/org/sleuthkit/autopsy/communications/images/marker--plus.png", false); + private static final String SINGULAR_TEXT = Bundle.PinAccountsAction_singularText(); + private static final String PLURAL_TEXT = Bundle.PinAccountsAction_pluralText(); + + private static final PinAccountsAction instance = new PinAccountsAction(); + + static PinAccountsAction getInstance() { + return instance; + } + + @Override + public void actionPerformed(ActionEvent event) { + CVTEvents.getCVTEventBus().post(new CVTEvents.PinAccountsEvent(getSelectedAccounts(), false)); + } + + @Override + protected String getActionDisplayName() { + return getSelectedAccounts().size() > 1 ? PLURAL_TEXT : SINGULAR_TEXT; + } + + @Override + ImageIcon getIcon() { + return ICON; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java b/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java index d5f7a7cc03..f7fb7d1232 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java @@ -19,22 +19,31 @@ package org.sleuthkit.autopsy.communications; import com.google.common.collect.ImmutableSet; +import com.google.common.eventbus.EventBus; import java.util.HashSet; import java.util.Set; +/** + * Model of what accounts are pinned to a visualization. + */ class PinnedAccountModel { /** - * Set of AccountDeviceInstanceKeys that are 'Pinned' to this graph. Pinned + * Set of AccountDeviceInstanceKeys that are 'Pinned' to the graph. Pinned * accounts are shown regardless of filters, and accounts that are related * to pinned accounts and pass the filters are show. Pinning accounts is the * primary way to populate the graph. */ private final Set pinnedAccountDevices = new HashSet<>(); - private final CommunicationsGraph graph; - PinnedAccountModel(CommunicationsGraph graph) { - this.graph = graph; + private final EventBus eventBus = new EventBus(); + + void registerhandler(Object handler) { + eventBus.register(handler); + } + + void unregisterhandler(Object handler) { + eventBus.unregister(handler); } boolean isAccountPinned(AccountDeviceInstanceKey account) { diff --git a/Core/src/org/sleuthkit/autopsy/communications/ResetAndPinAccountsAction.java b/Core/src/org/sleuthkit/autopsy/communications/ResetAndPinAccountsAction.java new file mode 100644 index 0000000000..385ac3348b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/ResetAndPinAccountsAction.java @@ -0,0 +1,59 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.communications; + +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import org.openide.util.ImageUtilities; +import org.openide.util.NbBundle; + +/** + * Action that clears any pinned accounts and pins the AcountDevicesInstanceKeys + * in the ActionsGlobalContext to the visualization. + */ +@NbBundle.Messages(value = {"ResetAndPinAccountsAction.singularText=Visualize Only Selected Account", + "ResetAndPinAccountsAction.pluralText=Visualize Only Selected Accounts"}) +final class ResetAndPinAccountsAction extends AbstractCVTAction { + + private static final ImageIcon ICON = ImageUtilities.loadImageIcon( + "/org/sleuthkit/autopsy/communications/images/marker--pin.png", false); + private static final String SINGULAR_TEXT = Bundle.ResetAndPinAccountsAction_singularText(); + private static final String PLURAL_TEXT = Bundle.ResetAndPinAccountsAction_pluralText(); + + private static final ResetAndPinAccountsAction instance = new ResetAndPinAccountsAction(); + + static ResetAndPinAccountsAction getInstance() { + return instance; + } + + @Override + public void actionPerformed(ActionEvent event) { + CVTEvents.getCVTEventBus().post(new CVTEvents.PinAccountsEvent(getSelectedAccounts(), true)); + } + + @Override + protected String getActionDisplayName() { + return getSelectedAccounts().size() > 1 ? PLURAL_TEXT : SINGULAR_TEXT; + } + + @Override + ImageIcon getIcon() { + return ICON; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/SelectionNode.java b/Core/src/org/sleuthkit/autopsy/communications/SelectionNode.java index 40739b49cc..1618bb9ecc 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/SelectionNode.java +++ b/Core/src/org/sleuthkit/autopsy/communications/SelectionNode.java @@ -24,10 +24,13 @@ import java.util.Collections; import java.util.List; import java.util.Set; import java.util.logging.Level; +import java.util.stream.Collectors; import org.openide.nodes.AbstractNode; import org.openide.nodes.ChildFactory; import org.openide.nodes.Children; import org.openide.nodes.Node; +import org.openide.util.Lookup; +import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.AccountDeviceInstance; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -44,23 +47,27 @@ import org.sleuthkit.datamodel.TskCoreException; */ final class SelectionNode extends AbstractNode { - private SelectionNode(Children children) { - super(children); + private SelectionNode(Children children, Lookup lookup) { + super(children, lookup); } static SelectionNode createFromAccountsAndRelationships( Set edgeRelationshipArtifacts, - Set accountDeviceInstances, + Set accountDeviceInstanceKeys, CommunicationsFilter filter, CommunicationsManager commsManager) { + Set accountDeviceInstances = accountDeviceInstanceKeys.stream() + .map(AccountDeviceInstanceKey::getAccountDeviceInstance) + .collect(Collectors.toSet()); + SelectionNode node = new SelectionNode(Children.create( new RelationshipChildren( edgeRelationshipArtifacts, accountDeviceInstances, commsManager, filter), - true)); + true), Lookups.fixed(accountDeviceInstanceKeys.toArray())); //This is not good for internationalization!!! String name = ""; @@ -82,7 +89,7 @@ final class SelectionNode extends AbstractNode { } static SelectionNode createFromAccounts( - Set accountDeviceInstances, + Set accountDeviceInstances, CommunicationsFilter filter, CommunicationsManager commsManager) { @@ -122,9 +129,9 @@ final class SelectionNode extends AbstractNode { } @Override - protected Node createNodeForKey(Content t) { - if (t instanceof BlackboardArtifact) { - return new RelationshipNode((BlackboardArtifact) t); + protected Node createNodeForKey(Content content) { + if (content instanceof BlackboardArtifact) { + return new RelationshipNode((BlackboardArtifact) content); } else { throw new UnsupportedOperationException("Cannot create a RelationshipNode for non BlackboardArtifact content."); } diff --git a/Core/src/org/sleuthkit/autopsy/communications/UnpinAccountsAction.java b/Core/src/org/sleuthkit/autopsy/communications/UnpinAccountsAction.java new file mode 100644 index 0000000000..ba0bbc545b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/UnpinAccountsAction.java @@ -0,0 +1,59 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.communications; + +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import org.openide.util.ImageUtilities; +import org.openide.util.NbBundle; + +/** + * Action that unpins the AcccountDeviceInstanceKeys in the ActionsGlobalContext + * form the visualization. + */ +@NbBundle.Messages({"UnpinAccountsAction.pluralText=Remove Selected Accounts", + "UnpinAccountsAction.singularText=Remove Selected Account"}) +final class UnpinAccountsAction extends AbstractCVTAction { + + static final private ImageIcon ICON = ImageUtilities.loadImageIcon( + "/org/sleuthkit/autopsy/communications/images/marker--minus.png", false); + private static final String SINGULAR_TEXT = Bundle.UnpinAccountsAction_singularText(); + private static final String PLURAL_TEXT = Bundle.UnpinAccountsAction_pluralText(); + + private static final UnpinAccountsAction instance = new UnpinAccountsAction(); + + static UnpinAccountsAction getInstance() { + return instance; + } + + @Override + public void actionPerformed(final ActionEvent event) { + CVTEvents.getCVTEventBus().post(new CVTEvents.UnpinAccountsEvent(getSelectedAccounts())); + } + + @Override + String getActionDisplayName() { + return getSelectedAccounts().size() > 1 ? PLURAL_TEXT : SINGULAR_TEXT; + } + + @Override + ImageIcon getIcon() { + return ICON; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form index 9d5722443e..a847f700c7 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form +++ b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form @@ -68,15 +68,15 @@ + + + - - - @@ -166,9 +166,6 @@ - - - @@ -179,9 +176,6 @@ - - - @@ -192,9 +186,6 @@ - - - @@ -205,9 +196,6 @@ - - - @@ -222,11 +210,11 @@ - - + + @@ -241,11 +229,11 @@ - - + + @@ -260,11 +248,11 @@ - - + + @@ -279,11 +267,11 @@ - - + + diff --git a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java index 77eabb55c7..4f6b53ae39 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java @@ -28,7 +28,6 @@ import com.mxgraph.model.mxCell; import com.mxgraph.model.mxICell; import com.mxgraph.swing.handler.mxRubberband; import com.mxgraph.swing.mxGraphComponent; -import com.mxgraph.swing.util.mxMorphing; import com.mxgraph.util.mxEvent; import com.mxgraph.util.mxEventObject; import com.mxgraph.util.mxEventSource; @@ -36,11 +35,14 @@ import com.mxgraph.util.mxPoint; import com.mxgraph.util.mxRectangle; import com.mxgraph.util.mxUndoManager; import com.mxgraph.util.mxUndoableEdit; +import com.mxgraph.view.mxCellState; import com.mxgraph.view.mxGraph; +import com.mxgraph.view.mxGraphView; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; +import java.awt.Font; import java.awt.Frame; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -51,18 +53,23 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyVetoException; import java.text.DecimalFormat; import java.util.Arrays; -import static java.util.Collections.singleton; import java.util.EnumSet; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.Map; import java.util.concurrent.Future; +import java.util.function.BiConsumer; import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.swing.AbstractAction; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JMenuItem; -import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JSplitPane; @@ -80,12 +87,11 @@ import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.lookup.ProxyLookup; import org.sleuthkit.autopsy.casemodule.Case; -import static org.sleuthkit.autopsy.casemodule.Case.Events.CURRENT_CASE; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.progress.ModalDialogProgressIndicator; -import org.sleuthkit.datamodel.AccountDeviceInstance; import org.sleuthkit.datamodel.CommunicationsFilter; import org.sleuthkit.datamodel.CommunicationsManager; import org.sleuthkit.datamodel.Content; @@ -101,23 +107,17 @@ import org.sleuthkit.datamodel.TskCoreException; * CVTTopComponent when this tab is active allowing for context sensitive * actions to work correctly. */ -@NbBundle.Messages("VisualizationPanel.cancelButton.text=Cancel") final public class VisualizationPanel extends JPanel implements Lookup.Provider { private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(VisualizationPanel.class.getName()); private static final String BASE_IMAGE_PATH = "/org/sleuthkit/autopsy/communications/images"; - static final private ImageIcon pinIcon - = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/marker--pin.png")); - static final private ImageIcon addPinIcon - = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/marker--plus.png")); - static final private ImageIcon unpinIcon - = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/marker--minus.png")); static final private ImageIcon unlockIcon = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/lock_large_unlocked.png")); static final private ImageIcon lockIcon = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/lock_large_locked.png")); + @NbBundle.Messages("VisualizationPanel.cancelButton.text=Cancel") private static final String CANCEL = Bundle.VisualizationPanel_cancelButton_text(); private final ExplorerManager vizEM = new ExplorerManager(); @@ -132,27 +132,20 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider private final CommunicationsGraph graph; private final mxUndoManager undoManager = new mxUndoManager(); - private final mxRubberband rubberband; - private final mxFastOrganicLayout fastOrganicLayout; - private final mxCircleLayout circleLayout; - private final mxOrganicLayout organicLayout; - private final mxHierarchicalLayout hierarchicalLayout; + private final mxRubberband rubberband; //NOPMD We keep a referenec as insurance to prevent garbage collection @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private SwingWorker worker; - private final PinnedAccountModel pinnedAccountModel; - private final LockedVertexModel lockedVertexModel; + private final PinnedAccountModel pinnedAccountModel = new PinnedAccountModel(); + private final LockedVertexModel lockedVertexModel = new LockedVertexModel(); + + private final Map layoutButtons = new HashMap<>(); + private NamedGraphLayout currentLayout; public VisualizationPanel() { initComponents(); - graph = new CommunicationsGraph(); - pinnedAccountModel = graph.getPinnedAccountModel(); - lockedVertexModel = graph.getLockedVertexModel(); - fastOrganicLayout = new mxFastOrganicLayoutImpl(graph); - circleLayout = new mxCircleLayoutImpl(graph); - organicLayout = new mxOrganicLayoutImpl(graph); - hierarchicalLayout = new mxHierarchicalLayoutImpl(graph); + graph = new CommunicationsGraph(pinnedAccountModel, lockedVertexModel); graphComponent = new mxGraphComponent(graph); graphComponent.setAutoExtend(true); @@ -166,123 +159,90 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider graphComponent.setBackground(Color.WHITE); borderLayoutPanel.add(graphComponent, BorderLayout.CENTER); - //install rubber band selection handler + //install rubber band other handlers rubberband = new mxRubberband(graphComponent); + lockedVertexModel.registerhandler(this); + final mxEventSource.mxIEventListener scaleListener = (Object sender, mxEventObject evt) -> zoomLabel.setText(DecimalFormat.getPercentInstance().format(graph.getView().getScale())); graph.getView().addListener(mxEvent.SCALE, scaleListener); graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, scaleListener); - graphComponent.getGraphControl().addMouseWheelListener(new MouseAdapter() { - /** - * Translate mouse wheel events into zooming. - * - * @param event The MouseWheelEvent - */ - @Override - public void mouseWheelMoved(final MouseWheelEvent event) { - super.mouseWheelMoved(event); - if (event.getPreciseWheelRotation() > 0) { - graphComponent.zoomIn(); - } else if (event.getPreciseWheelRotation() < 0) { - graphComponent.zoomOut(); - } - } - }); + final GraphMouseListener graphMouseListener = new GraphMouseListener(); + graphComponent.getGraphControl().addMouseWheelListener(graphMouseListener); + graphComponent.getGraphControl().addMouseListener(graphMouseListener); - graphComponent.getGraphControl().addMouseListener(new MouseAdapter() { - /** - * Right click handler: show context menu. - * - * @param event The MouseEvent - */ - @Override - public void mouseClicked(final MouseEvent event) { - super.mouseClicked(event); - if (SwingUtilities.isRightMouseButton(event)) { - final mxCell cellAt = (mxCell) graphComponent.getCellAt(event.getX(), event.getY()); - if (cellAt != null && cellAt.isVertex()) { - final JPopupMenu jPopupMenu = new JPopupMenu(); - final AccountDeviceInstanceKey adiKey = (AccountDeviceInstanceKey) cellAt.getValue(); - - if (lockedVertexModel.isVertexLocked(cellAt)) { - jPopupMenu.add(new JMenuItem(new AbstractAction("UnLock " + cellAt.getId(), unlockIcon) { - @Override - public void actionPerformed(final ActionEvent event) { - lockedVertexModel.unlockVertex(cellAt); - } - })); - } else { - jPopupMenu.add(new JMenuItem(new AbstractAction("Lock " + cellAt.getId(), lockIcon) { - @Override - public void actionPerformed(final ActionEvent event) { - lockedVertexModel.lockVertex(cellAt); - } - })); - } - if (pinnedAccountModel.isAccountPinned(adiKey)) { - jPopupMenu.add(new JMenuItem(new AbstractAction("Unpin " + cellAt.getId(), unpinIcon) { - @Override - public void actionPerformed(final ActionEvent event) { - handleUnPinEvent(new CVTEvents.UnpinAccountsEvent(singleton((AccountDeviceInstanceKey) cellAt.getValue()))); - } - })); - } else { - jPopupMenu.add(new JMenuItem(new AbstractAction("Pin " + cellAt.getId(), addPinIcon) { - @Override - public void actionPerformed(final ActionEvent event) { - handlePinEvent(new CVTEvents.PinAccountsEvent(singleton((AccountDeviceInstanceKey) cellAt.getValue()), false)); - } - })); - jPopupMenu.add(new JMenuItem(new AbstractAction("Pin only " + cellAt.getId(), pinIcon) { - @Override - public void actionPerformed(final ActionEvent event) { - handlePinEvent(new CVTEvents.PinAccountsEvent(singleton((AccountDeviceInstanceKey) cellAt.getValue()), true)); - } - })); - } - jPopupMenu.show(graphComponent.getGraphControl(), event.getX(), event.getY()); - } - } - } - }); final MessageBrowser messageBrowser = new MessageBrowser(vizEM, gacEM); - splitPane.setRightComponent(messageBrowser); - proxyLookup = new ProxyLookup( - messageBrowser.getLookup(), - ExplorerUtils.createLookup(vizEM, getActionMap())); + ExplorerUtils.createLookup(vizEM, getActionMap()), + messageBrowser.getLookup() + ); //feed selection to explorermanager - graph.getSelectionModel().addListener(null, new SelectionListener()); + graph.getSelectionModel().addListener(mxEvent.CHANGE, new SelectionListener()); final mxEventSource.mxIEventListener undoListener = (Object sender, mxEventObject evt) -> undoManager.undoableEditHappened((mxUndoableEdit) evt.getProperty("edit")); graph.getModel().addListener(mxEvent.UNDO, undoListener); graph.getView().addListener(mxEvent.UNDO, undoListener); + + FastOrganicLayoutImpl fastOrganicLayout = new FastOrganicLayoutImpl(graph); + CircleLayoutImpl circleLayout = new CircleLayoutImpl(graph); + OrganicLayoutImpl organicLayout = new OrganicLayoutImpl(graph); + organicLayout.setMaxIterations(10); + HierarchicalLayoutImpl hierarchyLayout = new HierarchicalLayoutImpl(graph); + + //local method to configure layout buttons + BiConsumer configure = (layoutButton, layout) -> { + layoutButtons.put(layout, layoutButton); + layoutButton.addActionListener(event -> applyLayout(layout)); + }; + //configure layout buttons. + configure.accept(circleLayoutButton, circleLayout); + configure.accept(organicLayoutButton, organicLayout); + configure.accept(fastOrganicLayoutButton, fastOrganicLayout); + configure.accept(hierarchyLayoutButton, hierarchyLayout); + + applyLayout(fastOrganicLayout); } + /** + * + * @param layoutButton the value of layoutButton + * @param layout the value of layout + */ @Override - public Lookup getLookup() { return proxyLookup; } @Subscribe - void handleUnPinEvent(final CVTEvents.UnpinAccountsEvent pinEvent) { + void handle(LockedVertexModel.VertexLockEvent event) { + final Set vertices = event.getVertices(); + mxGraphView view = graph.getView(); + vertices.forEach(vertex -> { + final mxCellState state = view.getState(vertex, true); + view.updateLabel(state); + view.updateLabelBounds(state); + view.updateBoundingBox(state); + graphComponent.redraw(state); + }); + } + + @Subscribe + void handle(final CVTEvents.UnpinAccountsEvent pinEvent) { graph.getModel().beginUpdate(); pinnedAccountModel.unpinAccount(pinEvent.getAccountDeviceInstances()); graph.clear(); rebuildGraph(); // Updates the display graph.getModel().endUpdate(); - } @Subscribe - void handlePinEvent(final CVTEvents.PinAccountsEvent pinEvent) { + void handle(final CVTEvents.PinAccountsEvent pinEvent) { graph.getModel().beginUpdate(); if (pinEvent.isReplace()) { graph.resetGraph(); @@ -291,19 +251,16 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider rebuildGraph(); // Updates the display graph.getModel().endUpdate(); - } @Subscribe - void handleFilterEvent(final CVTEvents.FilterChangeEvent filterChangeEvent) { - + void handle(final CVTEvents.FilterChangeEvent filterChangeEvent) { graph.getModel().beginUpdate(); graph.clear(); currentFilter = filterChangeEvent.getNewFilter(); rebuildGraph(); // Updates the display graph.getModel().endUpdate(); - } @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @@ -328,19 +285,12 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider if (worker.isCancelled()) { graph.resetGraph(); rebuildGraph(); - } else if (graph.getModel().getChildCount(graph.getDefaultParent()) < 64) { - applyOrganicLayout(10); - } else { - JOptionPane.showMessageDialog(this, - "Too many accounts, layout aborted.", - "Autopsy", - JOptionPane.WARNING_MESSAGE); } + applyLayout(currentLayout); } }); worker.execute(); - } } @@ -357,7 +307,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider logger.log(Level.SEVERE, "Can't get CommunicationsManager when there is no case open.", ex); } - Case.addEventTypeSubscriber(EnumSet.of(CURRENT_CASE), evt -> { + Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), evt -> { graph.getModel().beginUpdate(); try { graph.resetGraph(); @@ -374,15 +324,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider logger.log(Level.SEVERE, "Error getting CommunicationsManager for the current case.", ex); } } - }); } - @Override - public void removeNotify() { - super.removeNotify(); - } - /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always @@ -419,11 +363,11 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider borderLayoutPanel.setLayout(new BorderLayout()); + jTextArea1.setBackground(new Color(240, 240, 240)); jTextArea1.setColumns(20); jTextArea1.setLineWrap(true); jTextArea1.setRows(5); jTextArea1.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.jTextArea1.text")); // NOI18N - jTextArea1.setBackground(new Color(240, 240, 240)); GroupLayout placeHolderPanelLayout = new GroupLayout(placeHolderPanel); placeHolderPanel.setLayout(placeHolderPanelLayout); @@ -448,49 +392,29 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider hierarchyLayoutButton.setFocusable(false); hierarchyLayoutButton.setHorizontalTextPosition(SwingConstants.CENTER); hierarchyLayoutButton.setVerticalTextPosition(SwingConstants.BOTTOM); - hierarchyLayoutButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - hierarchyLayoutButtonActionPerformed(evt); - } - }); fastOrganicLayoutButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.fastOrganicLayoutButton.text")); // NOI18N fastOrganicLayoutButton.setFocusable(false); fastOrganicLayoutButton.setHorizontalTextPosition(SwingConstants.CENTER); fastOrganicLayoutButton.setVerticalTextPosition(SwingConstants.BOTTOM); - fastOrganicLayoutButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - fastOrganicLayoutButtonActionPerformed(evt); - } - }); organicLayoutButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.organicLayoutButton.text")); // NOI18N organicLayoutButton.setFocusable(false); organicLayoutButton.setHorizontalTextPosition(SwingConstants.CENTER); organicLayoutButton.setVerticalTextPosition(SwingConstants.BOTTOM); - organicLayoutButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - organicLayoutButtonActionPerformed(evt); - } - }); circleLayoutButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.circleLayoutButton.text")); // NOI18N circleLayoutButton.setFocusable(false); circleLayoutButton.setHorizontalTextPosition(SwingConstants.CENTER); circleLayoutButton.setVerticalTextPosition(SwingConstants.BOTTOM); - circleLayoutButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - circleLayoutButtonActionPerformed(evt); - } - }); jSeparator1.setOrientation(SwingConstants.VERTICAL); zoomOutButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/magnifier-zoom-out-red.png"))); // NOI18N zoomOutButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomOutButton.text")); // NOI18N + zoomOutButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomOutButton.toolTipText")); // NOI18N zoomOutButton.setFocusable(false); zoomOutButton.setHorizontalTextPosition(SwingConstants.CENTER); - zoomOutButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomOutButton.toolTipText")); // NOI18N zoomOutButton.setVerticalTextPosition(SwingConstants.BOTTOM); zoomOutButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { @@ -500,9 +424,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider zoomInButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/magnifier-zoom-in-green.png"))); // NOI18N zoomInButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomInButton.text")); // NOI18N + zoomInButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomInButton.toolTipText")); // NOI18N zoomInButton.setFocusable(false); zoomInButton.setHorizontalTextPosition(SwingConstants.CENTER); - zoomInButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomInButton.toolTipText")); // NOI18N zoomInButton.setVerticalTextPosition(SwingConstants.BOTTOM); zoomInButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { @@ -512,9 +436,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider zoomActualButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/magnifier-zoom-actual.png"))); // NOI18N zoomActualButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomActualButton.text")); // NOI18N + zoomActualButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomActualButton.toolTipText")); // NOI18N zoomActualButton.setFocusable(false); zoomActualButton.setHorizontalTextPosition(SwingConstants.CENTER); - zoomActualButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomActualButton.toolTipText")); // NOI18N zoomActualButton.setVerticalTextPosition(SwingConstants.BOTTOM); zoomActualButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { @@ -524,9 +448,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider fitZoomButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/magnifier-zoom-fit.png"))); // NOI18N fitZoomButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.fitZoomButton.text")); // NOI18N + fitZoomButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.fitZoomButton.toolTipText")); // NOI18N fitZoomButton.setFocusable(false); fitZoomButton.setHorizontalTextPosition(SwingConstants.CENTER); - fitZoomButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.fitZoomButton.toolTipText")); // NOI18N fitZoomButton.setVerticalTextPosition(SwingConstants.BOTTOM); fitZoomButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { @@ -626,21 +550,55 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider graphComponent.zoomOut(); }//GEN-LAST:event_zoomOutButtonActionPerformed - private void circleLayoutButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_circleLayoutButtonActionPerformed - morph(circleLayout); - }//GEN-LAST:event_circleLayoutButtonActionPerformed + /** + * Apply the given layout. The given layout becomes the current layout. The + * layout is computed in the background. + * + * @param layout The layout to apply. + */ + @NbBundle.Messages({"VisualizationPanel.computingLayout=Computing Layout", + "# {0} - layout name", + "VisualizationPanel.layoutFailWithLockedVertices.text={0} layout failed with locked vertices. Unlock some vertices or try a different layout.", + "# {0} - layout name", + "VisualizationPanel.layoutFail.text={0} layout failed. Try a different layout."}) + private void applyLayout(NamedGraphLayout layout) { + currentLayout = layout; + layoutButtons.forEach((layoutKey, button) + -> button.setFont(button.getFont().deriveFont(layoutKey == layout ? Font.BOLD : Font.PLAIN))); - private void organicLayoutButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_organicLayoutButtonActionPerformed - applyOrganicLayout(10); - }//GEN-LAST:event_organicLayoutButtonActionPerformed + ModalDialogProgressIndicator progressIndicator = new ModalDialogProgressIndicator(windowAncestor, Bundle.VisualizationPanel_computingLayout()); + progressIndicator.start(Bundle.VisualizationPanel_computingLayout()); - private void fastOrganicLayoutButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_fastOrganicLayoutButtonActionPerformed - morph(fastOrganicLayout); - }//GEN-LAST:event_fastOrganicLayoutButtonActionPerformed + new SwingWorker() { + @Override + protected Void doInBackground() { + graph.getModel().beginUpdate(); + try { + layout.execute(graph.getDefaultParent()); + fitGraph(); + } finally { + graph.getModel().endUpdate(); + progressIndicator.finish(); + } + return null; + } - private void hierarchyLayoutButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_hierarchyLayoutButtonActionPerformed - morph(hierarchicalLayout); - }//GEN-LAST:event_hierarchyLayoutButtonActionPerformed + @Override + protected void done() { + try { + get(); + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.WARNING, "CVT graph layout failed.", ex); + if (lockedVertexModel.isEmpty()) { + MessageNotifyUtil.Message.error(Bundle.VisualizationPanel_layoutFail_text(layout.getDisplayName())); + } else { + MessageNotifyUtil.Message.error(Bundle.VisualizationPanel_layoutFailWithLockedVertices_text(layout.getDisplayName())); + } + undoManager.undo(); + } + } + }.execute(); + } private void clearVizButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_clearVizButtonActionPerformed setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); @@ -651,14 +609,8 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider // Updates the display graph.getModel().endUpdate(); setCursor(Cursor.getDefaultCursor()); - }//GEN-LAST:event_clearVizButtonActionPerformed - private void applyOrganicLayout(int iterations) { - organicLayout.setMaxIterations(iterations); - morph(organicLayout); - } - private void fitGraph() { graphComponent.zoomTo(1, true); mxPoint translate = graph.getView().getTranslate(); @@ -681,57 +633,11 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider final Dimension size = graphComponent.getSize(); final double widthFactor = size.getWidth() / boundsForCells.getWidth(); + final double heightFactor = size.getHeight() / boundsForCells.getHeight(); - graphComponent.zoom(widthFactor); - + graphComponent.zoom((heightFactor + widthFactor) / 2.0); } - private void morph(mxIGraphLayout layout) { - // layout using morphing - graph.getModel().beginUpdate(); - - CancelationListener cancelationListener = new CancelationListener(); - ModalDialogProgressIndicator progress = new ModalDialogProgressIndicator(windowAncestor, "Computing layout", new String[]{CANCEL}, CANCEL, cancelationListener); - SwingWorker morphWorker = new SwingWorker() { - @Override - protected Void doInBackground() { - progress.start("Computing layout"); - layout.execute(graph.getDefaultParent()); - if (isCancelled()) { - progress.finish(); - return null; - } - mxMorphing morph = new mxMorphing(graphComponent, 20, 1.2, 20) { - @Override - public void updateAnimation() { - fireEvent(new mxEventObject(mxEvent.EXECUTE)); - super.updateAnimation(); //To change body of generated methods, choose Tools | Templates. - } - - }; - morph.addListener(mxEvent.EXECUTE, (Object sender, mxEventObject evt) -> { - if (isCancelled()) { - morph.stopAnimation(); - } - }); - morph.addListener(mxEvent.DONE, (Object sender, mxEventObject event) -> { - graph.getModel().endUpdate(); - if (isCancelled()) { - undoManager.undo(); - } else { - fitGraph(); - } - progress.finish(); - }); - - morph.startAnimation(); - return null; - - } - }; - cancelationListener.configure(morphWorker, progress); - morphWorker.execute(); - } // Variables declaration - do not modify//GEN-BEGIN:variables private JPanel borderLayoutPanel; @@ -755,6 +661,10 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider private JButton zoomOutButton; // End of variables declaration//GEN-END:variables + /** + * Listens to graph selection model and updates ExplorerManager to reflect + * changes in selection. + */ final private class SelectionListener implements mxEventSource.mxIEventListener { @SuppressWarnings("unchecked") @@ -766,7 +676,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider if (selectionCells.length > 0) { mxICell[] selectedCells = Arrays.asList(selectionCells).toArray(new mxCell[selectionCells.length]); HashSet relationshipSources = new HashSet<>(); - HashSet adis = new HashSet<>(); + HashSet adis = new HashSet<>(); for (mxICell cell : selectedCells) { if (cell.isEdge()) { mxICell source = (mxICell) graph.getModel().getTerminal(cell, true); @@ -783,7 +693,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider logger.log(Level.SEVERE, " Error getting relationsips....", tskCoreException); } } else if (cell.isVertex()) { - adis.add(((AccountDeviceInstanceKey) cell.getValue()).getAccountDeviceInstance()); + adis.add((AccountDeviceInstanceKey) cell.getValue()); } } @@ -799,9 +709,20 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } } - final private class mxFastOrganicLayoutImpl extends mxFastOrganicLayout { + /** + * Extend mxIGraphLayout with a getDisplayName method, + */ + private interface NamedGraphLayout extends mxIGraphLayout { - mxFastOrganicLayoutImpl(mxGraph graph) { + String getDisplayName(); + } + + /** + * Extension of mxFastOrganicLayout that ignores locked vertices. + */ + final private class FastOrganicLayoutImpl extends mxFastOrganicLayout implements NamedGraphLayout { + + FastOrganicLayoutImpl(mxGraph graph) { super(graph); } @@ -812,18 +733,26 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } @Override - public mxRectangle setVertexLocation(Object vertex, double x, double y) { + public mxRectangle setVertexLocation(Object vertex, double x, double y) { //NOPMD x, y are standard coordinate names if (isVertexIgnored(vertex)) { return getVertexBounds(vertex); } else { return super.setVertexLocation(vertex, x, y); } } + + @Override + public String getDisplayName() { + return "Fast Organic"; + } } - final private class mxCircleLayoutImpl extends mxCircleLayout { + /** + * Extension of mxCircleLayout that ignores locked vertices. + */ + final private class CircleLayoutImpl extends mxCircleLayout implements NamedGraphLayout { - mxCircleLayoutImpl(mxGraph graph) { + CircleLayoutImpl(mxGraph graph) { super(graph); setResetEdges(true); } @@ -835,18 +764,26 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } @Override - public mxRectangle setVertexLocation(Object vertex, double x, double y) { + public mxRectangle setVertexLocation(Object vertex, double x, double y) { //NOPMD x, y are standard coordinate names if (isVertexIgnored(vertex)) { return getVertexBounds(vertex); } else { return super.setVertexLocation(vertex, x, y); } } + + @Override + public String getDisplayName() { + return "Circle"; + } } - final private class mxOrganicLayoutImpl extends mxOrganicLayout { + /** + * Extension of mxOrganicLayout that ignores locked vertices. + */ + final private class OrganicLayoutImpl extends mxOrganicLayout implements NamedGraphLayout { - mxOrganicLayoutImpl(mxGraph graph) { + OrganicLayoutImpl(mxGraph graph) { super(graph); setResetEdges(true); } @@ -858,18 +795,26 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } @Override - public mxRectangle setVertexLocation(Object vertex, double x, double y) { + public mxRectangle setVertexLocation(Object vertex, double x, double y) { //NOPMD x, y are standard coordinate names if (isVertexIgnored(vertex)) { return getVertexBounds(vertex); } else { return super.setVertexLocation(vertex, x, y); } } + + @Override + public String getDisplayName() { + return "Organic"; + } } - final private class mxHierarchicalLayoutImpl extends mxHierarchicalLayout { + /** + * Extension of mxHierarchicalLayout that ignores locked vertices. + */ + final private class HierarchicalLayoutImpl extends mxHierarchicalLayout implements NamedGraphLayout { - mxHierarchicalLayoutImpl(mxGraph graph) { + HierarchicalLayoutImpl(mxGraph graph) { super(graph); } @@ -880,15 +825,24 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } @Override - public mxRectangle setVertexLocation(Object vertex, double x, double y) { + public mxRectangle setVertexLocation(Object vertex, double x, double y) { //NOPMD x, y are standard coordinate names if (isVertexIgnored(vertex)) { return getVertexBounds(vertex); } else { return super.setVertexLocation(vertex, x, y); } } + + @Override + public String getDisplayName() { + return "Hierarchical"; + } } + /** + * Listener that closses the given ModalDialogProgressIndicator and cancels + * the future. + */ private class CancelationListener implements ActionListener { private Future cancellable; @@ -905,6 +859,108 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider cancellable.cancel(true); progress.finish(); } + } + /** + * Mouse Adapter for the graphComponent. Handles wheel zooming and context + * menus. + */ + private class GraphMouseListener extends MouseAdapter { + + /** + * Translate mouse wheel events into zooming. + * + * @param event The MouseWheelEvent + */ + @Override + public void mouseWheelMoved(final MouseWheelEvent event) { + super.mouseWheelMoved(event); + if (event.getPreciseWheelRotation() < 0) { + graphComponent.zoomIn(); + } else if (event.getPreciseWheelRotation() > 0) { + graphComponent.zoomOut(); + } + } + + /** + * Right click handler: show context menu. + * + * @param event The MouseEvent + */ + @Override + public void mouseClicked(final MouseEvent event) { + super.mouseClicked(event); + if (SwingUtilities.isRightMouseButton(event)) { + final mxCell cellAt = (mxCell) graphComponent.getCellAt(event.getX(), event.getY()); + if (cellAt != null && cellAt.isVertex()) { + final JPopupMenu jPopupMenu = new JPopupMenu(); + final AccountDeviceInstanceKey adiKey = (AccountDeviceInstanceKey) cellAt.getValue(); + + Set selectedVertices + = Stream.of(graph.getSelectionModel().getCells()) + .map(mxCell.class::cast) + .filter(mxCell::isVertex) + .collect(Collectors.toSet()); + + if (lockedVertexModel.isVertexLocked(cellAt)) { + jPopupMenu.add(new JMenuItem(new UnlockAction(selectedVertices))); + } else { + jPopupMenu.add(new JMenuItem(new LockAction(selectedVertices))); + } + if (pinnedAccountModel.isAccountPinned(adiKey)) { + jPopupMenu.add(UnpinAccountsAction.getInstance().getPopupPresenter()); + } else { + jPopupMenu.add(PinAccountsAction.getInstance().getPopupPresenter()); + jPopupMenu.add(ResetAndPinAccountsAction.getInstance().getPopupPresenter()); + } + jPopupMenu.show(graphComponent.getGraphControl(), event.getX(), event.getY()); + } + } + } + } + + /** + * Action that un-locks the selected vertices. + */ + @NbBundle.Messages({ + "VisualizationPanel.unlockAction.singularText=Unlock Selected Account", + "VisualizationPanel.unlockAction.pluralText=Unlock Selected Accounts",}) + private final class UnlockAction extends AbstractAction { + + private final Set selectedVertices; + + UnlockAction(Set selectedVertices) { + super(selectedVertices.size() > 1 ? Bundle.VisualizationPanel_unlockAction_pluralText() : Bundle.VisualizationPanel_unlockAction_singularText(), + unlockIcon); + this.selectedVertices = selectedVertices; + } + + @Override + + public void actionPerformed(final ActionEvent event) { + lockedVertexModel.unlock(selectedVertices); + } + } + + /** + * Action that locks the selected vertices. + */ + @NbBundle.Messages({ + "VisualizationPanel.lockAction.singularText=Lock Selected Account", + "VisualizationPanel.lockAction.pluralText=Lock Selected Accounts"}) + private final class LockAction extends AbstractAction { + + private final Set selectedVertices; + + LockAction(Set selectedVertices) { + super(selectedVertices.size() > 1 ? Bundle.VisualizationPanel_lockAction_pluralText() : Bundle.VisualizationPanel_lockAction_singularText(), + lockIcon); + this.selectedVertices = selectedVertices; + } + + @Override + public void actionPerformed(final ActionEvent event) { + lockedVertexModel.lock(selectedVertices); + } } } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.java index 7561e3fa75..ce2721e647 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.java @@ -503,7 +503,6 @@ public class MessageContentViewer extends javax.swing.JPanel implements DataCont rtfbodyTextPane.setText(""); htmlbodyTextPane.setText(""); textbodyTextArea.setText(""); - drp.setNode(null); showImagesToggleButton.setEnabled(false); msgbodyTabbedPane.setEnabled(false); } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties index 535c88ce61..245fe1744d 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties @@ -24,7 +24,7 @@ DataContentViewerString.pageLabel2.text=Page # Product Information panel LBL_Description=
\n Product Version: {0} ({9})
Sleuth Kit Version: {7}
Netbeans RCP Build: {8}
Java: {1}; {2}
System: {3}; {4}; {5}
Userdir: {6}
Format_OperatingSystem_Value={0} version {1} running on {2} -LBL_Copyright=
Autopsy™ is a digital forensics platform based on The Sleuth Kit™ and other tools.
Copyright © 2003-2017.
+LBL_Copyright=
Autopsy™ is a digital forensics platform based on The Sleuth Kit™ and other tools.
Copyright © 2003-2018.
URL_ON_IMG=http://www.sleuthkit.org/ URL_ON_HELP=http://sleuthkit.org/autopsy/docs/user-docs/4.6.0/ FILE_FOR_LOCAL_HELP=file:/// diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java b/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java index 3b6878ce23..a06d3eeeea 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java @@ -408,6 +408,7 @@ final public class Accounts implements AutopsyVisitableItem { protected void addNotify() { IngestManager.getInstance().addIngestJobEventListener(pcl); IngestManager.getInstance().addIngestModuleEventListener(pcl); + Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl); super.addNotify(); } @@ -415,6 +416,7 @@ final public class Accounts implements AutopsyVisitableItem { protected void removeNotify() { IngestManager.getInstance().removeIngestJobEventListener(pcl); IngestManager.getInstance().removeIngestModuleEventListener(pcl); + Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl); super.removeNotify(); } @@ -569,6 +571,7 @@ final public class Accounts implements AutopsyVisitableItem { protected void addNotify() { IngestManager.getInstance().addIngestJobEventListener(pcl); IngestManager.getInstance().addIngestModuleEventListener(pcl); + Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl); super.addNotify(); } @@ -576,6 +579,7 @@ final public class Accounts implements AutopsyVisitableItem { protected void removeNotify() { IngestManager.getInstance().removeIngestJobEventListener(pcl); IngestManager.getInstance().removeIngestModuleEventListener(pcl); + Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl); super.removeNotify(); } @@ -691,6 +695,7 @@ final public class Accounts implements AutopsyVisitableItem { protected void addNotify() { IngestManager.getInstance().addIngestJobEventListener(pcl); IngestManager.getInstance().addIngestModuleEventListener(pcl); + Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl); super.addNotify(); } @@ -698,6 +703,7 @@ final public class Accounts implements AutopsyVisitableItem { protected void removeNotify() { IngestManager.getInstance().removeIngestJobEventListener(pcl); IngestManager.getInstance().removeIngestModuleEventListener(pcl); + Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl); super.removeNotify(); } @@ -901,6 +907,7 @@ final public class Accounts implements AutopsyVisitableItem { protected void addNotify() { IngestManager.getInstance().addIngestJobEventListener(pcl); IngestManager.getInstance().addIngestModuleEventListener(pcl); + Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl); super.addNotify(); } @@ -908,6 +915,7 @@ final public class Accounts implements AutopsyVisitableItem { protected void removeNotify() { IngestManager.getInstance().removeIngestJobEventListener(pcl); IngestManager.getInstance().removeIngestModuleEventListener(pcl); + Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl); super.removeNotify(); } diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/Bundle.properties b/Core/src/org/sleuthkit/autopsy/healthmonitor/Bundle.properties index 4185424161..7c8a50c757 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/Bundle.properties @@ -18,3 +18,4 @@ TestPanel.jLabel5.text=Number of nodes TestPanel.nNodesTextField.text= TestPanel.jButton2.text=Populate DB TestPanel.newGraphButton.text=New graph! +TestPanel.jButton3.text=Make blocky data diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/ServicesHealthMonitor.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java similarity index 81% rename from Core/src/org/sleuthkit/autopsy/healthmonitor/ServicesHealthMonitor.java rename to Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java index c996f39752..6d7825c10d 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/ServicesHealthMonitor.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java @@ -19,6 +19,8 @@ package org.sleuthkit.autopsy.healthmonitor; import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; @@ -29,17 +31,24 @@ import java.util.Map; 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; 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.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; @@ -50,30 +59,46 @@ import org.sleuthkit.datamodel.CaseDbSchemaVersionNumber; * Modules will call getTimingMetric() before the code to be timed to get a TimingMetric object * Modules will call submitTimingMetric() with the obtained TimingMetric object to log it */ -public class ServicesHealthMonitor { +public final class EnterpriseHealthMonitor implements PropertyChangeListener { - private final static Logger logger = Logger.getLogger(ServicesHealthMonitor.class.getName()); - private final static String DATABASE_NAME = "ServicesHealthMonitor"; - private final static String MODULE_NAME = "ServicesHealthMonitor"; + 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 = 1; // Minutes TODO - put back to an hour public static final CaseDbSchemaVersionNumber CURRENT_DB_SCHEMA_VERSION = new CaseDbSchemaVersionNumber(1, 0); private static final AtomicBoolean isEnabled = new AtomicBoolean(false); - private static ServicesHealthMonitor instance; + private static EnterpriseHealthMonitor instance; - private ScheduledThreadPoolExecutor periodicTasksExecutor; + private final ExecutorService healthMonitorExecutor; + private static final String HEALTH_MONITOR_EVENT_THREAD_NAME = "Health-Monitor-Event-Listener-%d"; + + private ScheduledThreadPoolExecutor healthMonitorOutputTimer; private final Map timingInfoMap; private static final int CONN_POOL_SIZE = 10; private BasicDataSource connectionPool = null; + private String hostName; - private ServicesHealthMonitor() throws HealthMonitorException { + private EnterpriseHealthMonitor() throws HealthMonitorException { // Create the map to collect timing metrics. The map will exist regardless // of whether the monitor is enabled. timingInfoMap = new HashMap<>(); + // Set up the executor to handle case events + healthMonitorExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat(HEALTH_MONITOR_EVENT_THREAD_NAME).build()); + + // Get the host name + try { + hostName = java.net.InetAddress.getLocalHost().getHostName(); + } catch (java.net.UnknownHostException ex) { + // Continue on, but log the error and generate a UUID to use for this session + hostName = UUID.randomUUID().toString(); + 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")){ @@ -93,13 +118,14 @@ public class ServicesHealthMonitor { } /** - * Get the instance of the ServicesHealthMonitor + * Get the instance of the EnterpriseHealthMonitor * @return the instance * @throws HealthMonitorException */ - synchronized static ServicesHealthMonitor getInstance() throws HealthMonitorException { + synchronized static EnterpriseHealthMonitor getInstance() throws HealthMonitorException { if (instance == null) { - instance = new ServicesHealthMonitor(); + instance = new EnterpriseHealthMonitor(); + Case.addPropertyChangeListener(instance); } return instance; } @@ -118,15 +144,15 @@ public class ServicesHealthMonitor { shutdownConnections(); if (!UserPreferences.getIsMultiUserModeEnabled()) { - throw new HealthMonitorException("Multi user mode is not enabled - can not activate services health monitor"); - } - // Set up database (if needed) - CoordinationService.Lock lock = getExclusiveDbLock(); - if(lock == null) { - throw new HealthMonitorException("Error getting database lock"); + throw new HealthMonitorException("Multi user mode is not enabled - can not activate health monitor"); } - try { + // Set up database (if needed) + try (CoordinationService.Lock lock = getExclusiveDbLock()) { + if(lock == null) { + throw new HealthMonitorException("Error getting database lock"); + } + // Check if the database exists if (! databaseExists()) { @@ -138,12 +164,8 @@ public class ServicesHealthMonitor { initializeDatabaseSchema(); } - } finally { - try { - lock.release(); - } catch (CoordinationService.CoordinationServiceException ex) { - throw new HealthMonitorException("Error releasing database lock", ex); - } + } catch (CoordinationService.CoordinationServiceException ex) { + throw new HealthMonitorException("Error releasing database lock", ex); } // Clear out any old data @@ -178,20 +200,19 @@ public class ServicesHealthMonitor { * Start the ScheduledThreadPoolExecutor that will handle the database writes. */ private synchronized void startTimer() { - if(periodicTasksExecutor != null) { - // Make sure the previous executor (if it exists) has been stopped - periodicTasksExecutor.shutdown(); - } - periodicTasksExecutor = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat("health_monitor_timer").build()); - periodicTasksExecutor.scheduleWithFixedDelay(new DatabaseWriteTask(), DATABASE_WRITE_INTERVAL, DATABASE_WRITE_INTERVAL, TimeUnit.MINUTES); + // Make sure the previous executor (if it exists) has been stopped + stopTimer(); + + healthMonitorOutputTimer = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat("health_monitor_timer").build()); + healthMonitorOutputTimer.scheduleWithFixedDelay(new DatabaseWriteTask(), DATABASE_WRITE_INTERVAL, DATABASE_WRITE_INTERVAL, TimeUnit.MINUTES); } /** * Stop the ScheduledThreadPoolExecutor to prevent further database writes. */ private synchronized void stopTimer() { - if(periodicTasksExecutor != null) { - periodicTasksExecutor.shutdown(); + if(healthMonitorOutputTimer != null) { + ThreadUtils.shutDownTaskExecutor(healthMonitorOutputTimer); } } @@ -199,7 +220,7 @@ public class ServicesHealthMonitor { * Called from the installer to set up the Health Monitor instance at startup. * @throws HealthMonitorException */ - static synchronized void startUp() throws HealthMonitorException { + static synchronized void startUpIfEnabled() throws HealthMonitorException { getInstance(); } @@ -231,7 +252,7 @@ public class ServicesHealthMonitor { * Get a metric that will measure the time to execute a section of code. * Call this before the section of code to be timed and then * submit it afterward using submitTimingMetric(). - * This method is safe to call regardless of whether the Services Health + * This method is safe to call regardless of whether the Enterprise Health * Monitor is enabled. * @param name A short but descriptive name describing the code being timed. * This name will appear in the UI. @@ -247,7 +268,7 @@ public class ServicesHealthMonitor { /** * Submit the metric that was previously obtained through getTimingMetric(). * Call this immediately after the section of code being timed. - * This method is safe to call regardless of whether the Services Health + * This method is safe to call regardless of whether the Enterprise Health * Monitor is enabled. * @param metric The TimingMetric object obtained from getTimingMetric() */ @@ -306,29 +327,20 @@ public class ServicesHealthMonitor { timingMapCopy = new HashMap<>(timingInfoMap); timingInfoMap.clear(); } - logger.log(Level.INFO, "Writing health monitor metrics to database"); - - String hostName; - try { - hostName = java.net.InetAddress.getLocalHost().getHostName(); - } catch (java.net.UnknownHostException ex) { - // Write it to the database but log a warning - logger.log(Level.WARNING, "Unable to look up host name"); - hostName = "unknown"; - } // Check if there's anything to report (right now we only have the timing map) if(timingMapCopy.keySet().isEmpty()) { return; } - // Write to the database - CoordinationService.Lock lock = getSharedDbLock(); - if(lock == null) { - throw new HealthMonitorException("Error getting database lock"); - } + logger.log(Level.INFO, "Writing health monitor metrics to database"); - try { + // Write to the database + 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"); @@ -361,12 +373,8 @@ public class ServicesHealthMonitor { logger.log(Level.SEVERE, "Error closing Connection.", ex); } } - } finally { - try { - lock.release(); - } catch (CoordinationService.CoordinationServiceException ex) { - throw new HealthMonitorException("Error releasing database lock", ex); - } + } catch (CoordinationService.CoordinationServiceException ex) { + throw new HealthMonitorException("Error releasing database lock", ex); } } @@ -385,10 +393,9 @@ public class ServicesHealthMonitor { try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS Statement statement = connection.createStatement();) { String createCommand = "SELECT 1 AS result FROM pg_database WHERE datname='" + DATABASE_NAME + "'"; - System.out.println(" query: " + createCommand); rs = statement.executeQuery(createCommand); if(rs.next()) { - logger.log(Level.INFO, "Existing Services Health Monitor database found"); + logger.log(Level.INFO, "Existing Enterprise Health Monitor database found"); return true; } } finally { @@ -593,26 +600,26 @@ public class ServicesHealthMonitor { try (Statement statement = conn.createStatement()) { conn.setAutoCommit(false); - StringBuilder createTimingTable = new StringBuilder(); - createTimingTable.append("CREATE TABLE IF NOT EXISTS timing_data ("); - createTimingTable.append("id SERIAL PRIMARY KEY,"); - createTimingTable.append("name text NOT NULL,"); - createTimingTable.append("host text NOT NULL,"); - createTimingTable.append("timestamp bigint NOT NULL,"); - createTimingTable.append("count bigint NOT NULL,"); - createTimingTable.append("average bigint NOT NULL,"); - createTimingTable.append("max bigint NOT NULL,"); - createTimingTable.append("min bigint NOT NULL"); - createTimingTable.append(")"); - statement.execute(createTimingTable.toString()); + String createTimingTable = + "CREATE TABLE IF NOT EXISTS timing_data (" + + "id SERIAL PRIMARY KEY," + + "name text NOT NULL," + + "host text NOT NULL," + + "timestamp bigint NOT NULL," + + "count bigint NOT NULL," + + "average bigint NOT NULL," + + "max bigint NOT NULL," + + "min bigint NOT NULL" + + ")"; + statement.execute(createTimingTable); - StringBuilder createDbInfoTable = new StringBuilder(); - createDbInfoTable.append("CREATE TABLE IF NOT EXISTS db_info ("); - createDbInfoTable.append("id SERIAL PRIMARY KEY NOT NULL,"); - createDbInfoTable.append("name text NOT NULL,"); - createDbInfoTable.append("value text NOT NULL"); - createDbInfoTable.append(")"); - statement.execute(createDbInfoTable.toString()); + String createDbInfoTable = + "CREATE TABLE IF NOT EXISTS db_info (" + + "id SERIAL PRIMARY KEY NOT NULL," + + "name text NOT NULL," + + "value text NOT NULL" + + ")"; + statement.execute(createDbInfoTable); 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() + "')"); @@ -653,11 +660,25 @@ public class ServicesHealthMonitor { } } + @Override + public void propertyChange(PropertyChangeEvent evt) { + + switch (Case.Events.valueOf(evt.getPropertyName())) { + + 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()); + } + break; + } + } + /** * TODO: remove this - testing only * Will put a bunch of sample data into the database */ - final void populateDatabase(int nDays, int nNodes) throws HealthMonitorException { + final void populateDatabase(int nDays, int nNodes, boolean createVerificationData) throws HealthMonitorException { if(! isEnabled.get()) { throw new HealthMonitorException("Can't populate database - monitor not enabled"); @@ -708,16 +729,27 @@ public class ServicesHealthMonitor { long aveTime; - // Make different cases - int outlierVal = rand.nextInt(30); - if(outlierVal < 2){ - aveTime = minIndexTime + maxIndexTimeOverMin + rand.nextInt(maxIndexTimeOverMin); - } else if(outlierVal == 2){ - aveTime = (minIndexTime / 2) + rand.nextInt(minIndexTime / 2); - } else if(outlierVal < 17) { - aveTime = minIndexTime + (rand.nextInt(maxIndexTimeOverMin) / 2); + 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); + if(outlierVal < 2){ + aveTime = minIndexTime + maxIndexTimeOverMin + rand.nextInt(maxIndexTimeOverMin); + } else if(outlierVal == 2){ + aveTime = (minIndexTime / 2) + rand.nextInt(minIndexTime / 2); + } else if(outlierVal < 17) { + aveTime = minIndexTime + (rand.nextInt(maxIndexTimeOverMin) / 2); + } else { + aveTime = minIndexTime + rand.nextInt(maxIndexTimeOverMin); + } } else { - aveTime = minIndexTime + rand.nextInt(maxIndexTimeOverMin); + // 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; } @@ -736,14 +768,27 @@ public class ServicesHealthMonitor { // Record index chunk every hour for(long timestamp = minTimestamp + rand.nextInt(1000 * 60 * 55);timestamp < maxTimestamp;timestamp += (1 + rand.nextInt(10)) * millisPerHour) { - long aveTime = minConnTime + rand.nextInt(maxConnTimeOverMin); - - // Check if we should make an outlier - int outlierVal = rand.nextInt(30); - if(outlierVal < 2){ - aveTime = minConnTime + maxConnTimeOverMin + rand.nextInt(maxConnTimeOverMin); - } else if(outlierVal == 8){ - aveTime = (minConnTime / 2) + rand.nextInt(minConnTime / 2); + long aveTime; + if( ! createVerificationData ) { + // Try to make a reasonable sample data set, with most points in a small range + // but some higher and lower + aveTime = minConnTime + rand.nextInt(maxConnTimeOverMin); + + // Check if we should make an outlier + int outlierVal = rand.nextInt(30); + if(outlierVal < 2){ + aveTime = minConnTime + maxConnTimeOverMin + rand.nextInt(maxConnTimeOverMin); + } else if(outlierVal == 8){ + aveTime = (minConnTime / 2) + rand.nextInt(minConnTime / 2); + } + } 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, "Solr: Connectivity check"); @@ -857,15 +902,14 @@ public class ServicesHealthMonitor { */ private CoordinationService.Lock getExclusiveDbLock() throws HealthMonitorException{ try { - String databaseNodeName = DATABASE_NAME; - CoordinationService.Lock lock = CoordinationService.getInstance().tryGetExclusiveLock(CoordinationService.CategoryNode.HEALTH_MONITOR, databaseNodeName, 5, TimeUnit.MINUTES); + CoordinationService.Lock lock = CoordinationService.getInstance().tryGetExclusiveLock(CoordinationService.CategoryNode.HEALTH_MONITOR, DATABASE_NAME, 5, TimeUnit.MINUTES); if(lock != null){ return lock; } throw new HealthMonitorException("Error acquiring database lock"); } catch (InterruptedException | CoordinationService.CoordinationServiceException ex){ - throw new HealthMonitorException("Error acquiring database lock"); + throw new HealthMonitorException("Error acquiring database lock", ex); } } @@ -973,14 +1017,16 @@ public class ServicesHealthMonitor { */ static class DatabaseTimingResult { private long timestamp; // Time the metric was recorded + private String hostname; // Host that recorded the metric private long count; // Number of metrics collected private double average; // Average of the durations collected (milliseconds) private double max; // Maximum value found (milliseconds) private double min; // Minimum value found (milliseconds) // TODO - maybe delete this - DatabaseTimingResult(long timestamp, long count, double average, double max, double min) { + DatabaseTimingResult(long timestamp, String hostname, long count, double average, double max, double min) { this.timestamp = timestamp; + this.hostname = hostname; this.count = count; this.average = average; this.max = max; @@ -989,6 +1035,7 @@ public class ServicesHealthMonitor { DatabaseTimingResult(ResultSet resultSet) throws SQLException { this.timestamp = resultSet.getLong("timestamp"); + this.hostname = resultSet.getString("host"); this.count = resultSet.getLong("count"); this.average = resultSet.getLong("average") / 1000000; this.max = resultSet.getLong("max") / 1000000; @@ -1034,5 +1081,13 @@ public class ServicesHealthMonitor { long getCount() { return count; } + + /** + * Get the name of the host that recorded this metric + * @return the host + */ + String getHostName() { + return hostname; + } } } diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorCaseEventListener.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorCaseEventListener.java deleted file mode 100644 index 5c8a6f675d..0000000000 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorCaseEventListener.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2018 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.healthmonitor; - -import com.google.common.util.concurrent.ThreadFactoryBuilder; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import org.sleuthkit.autopsy.casemodule.Case; - -/** - * Listener for case events - */ -final class HealthMonitorCaseEventListener implements PropertyChangeListener { - - private final ExecutorService jobProcessingExecutor; - private static final String CASE_EVENT_THREAD_NAME = "Health-Monitor-Event-Listener-%d"; - - HealthMonitorCaseEventListener() { - jobProcessingExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat(CASE_EVENT_THREAD_NAME).build()); - } - - @Override - public void propertyChange(PropertyChangeEvent evt) { - - switch (Case.Events.valueOf(evt.getPropertyName())) { - - case CURRENT_CASE: - if ((null == evt.getNewValue()) && (evt.getOldValue() instanceof Case)) { - // When a case is closed, write the current metrics to the database - jobProcessingExecutor.submit(new ServicesHealthMonitor.DatabaseWriteTask()); - } - break; - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java index 189373c32d..49fe2251c3 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java @@ -19,17 +19,25 @@ package org.sleuthkit.autopsy.healthmonitor; import com.mchange.v2.cfg.DelayedLogItem; +import java.awt.Component; import java.awt.Dimension; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.HashSet; +import java.util.HashMap; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.Font; +import javax.swing.Box; 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; @@ -48,51 +56,96 @@ public class HealthMonitorDashboard { private final static Logger logger = Logger.getLogger(HealthMonitorDashboard.class.getName()); - //Map> timingData; + Map> timingData; private JPanel timingMetricPanel = null; private JPanel timingButtonPanel = null; private JComboBox dateComboBox = null; + private JComboBox hostComboBox = null; + private JCheckBox hostCheckBox = null; HealthMonitorDashboard() { - + timingData = new HashMap<>(); } void display() throws HealthMonitorException { - // Initialize and populate the timing metric panel - populateTimingMetricPanel(); + // Get a copy of the timing data from the database + timingData = EnterpriseHealthMonitor.getInstance().getTimingMetricsFromDatabase(); + + // Set up the buttons + setupTimingButtonPanel(); addActionListeners(); - System.out.println("Creating dialog"); + // Initialize and populate the timing metric panel + populateTimingMetricPanel(); + JScrollPane scrollPane = new JScrollPane(timingMetricPanel); JDialog dialog = new JDialog(); - dialog.setPreferredSize(new Dimension(1500, 800)); - dialog.setTitle("Services Health Monitor"); + //dialog.setPreferredSize(new Dimension(1500, 800)); + dialog.setTitle("Enterprise Health Monitor"); //dialog.add(graphPanel); dialog.add(scrollPane); dialog.pack(); dialog.setVisible(true); - System.out.println("Done displaying dialog"); } /** - * Initialize the panel holding the timing metric controls, - * if it has not already been initialized. + * Initialize the panel holding the timing metric controls and + * update components */ - private void initializeTimingButtonPanel() throws HealthMonitorException { + private void setupTimingButtonPanel() throws HealthMonitorException { if(timingButtonPanel == null) { + timingButtonPanel = new JPanel(); + timingButtonPanel.setBorder(BorderFactory.createEtchedBorder()); + //timingButtonPanel.setPreferredSize(new Dimension(1500, 100)); + } else { + timingButtonPanel.removeAll(); + } + + // The contents of the date combo box will never change, so we only need to + // do this once + if(dateComboBox == null) { // 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.TWO_WEEKS.getLabel()); - - // Add the date range button and label to the panel - timingButtonPanel = new JPanel(); - timingButtonPanel.setBorder(BorderFactory.createEtchedBorder()); - timingButtonPanel.add(new JLabel("Max days to display")); - timingButtonPanel.add(dateComboBox); } + + // Create an array of host names + Set 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()])); + + // Create the checkbox (if needed) + if(hostCheckBox == null) { + hostCheckBox = new JCheckBox("Filter by host"); + hostCheckBox.setSelected(false); + hostComboBox.setEnabled(false); + } + + // Create the panel + timingButtonPanel = new JPanel(); + timingButtonPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + //timingButtonPanel.setBorder(BorderFactory.createEtchedBorder()); + + // Add the date range combo box and label to the panel + timingButtonPanel.add(new JLabel("Max days to display")); + timingButtonPanel.add(dateComboBox); + + // Put some space between the elements + timingButtonPanel.add(Box.createHorizontalStrut(100)); + + // Add the host combo box and checkbox to the panel + timingButtonPanel.add(hostCheckBox); + //timingButtonPanel.add(new JLabel("Host to display")); + timingButtonPanel.add(hostComboBox); } /** @@ -100,18 +153,52 @@ public class HealthMonitorDashboard { * Call this after all components are initialized. */ private void addActionListeners() { + // Set up a listener on the combo box that will update the timing // metric graphs - dateComboBox.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent arg0) { - try { - populateTimingMetricPanel(); - } catch (HealthMonitorException ex) { - logger.log(Level.SEVERE, "Error populating timing metric panel", ex); + if((dateComboBox != null) && (dateComboBox.getActionListeners().length == 0)) { + dateComboBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + try { + populateTimingMetricPanel(); + } catch (HealthMonitorException ex) { + logger.log(Level.SEVERE, "Error populating timing metric panel", ex); + } } - } - }); + }); + } + + // Set up a listener on the host name combo box + if((hostComboBox != null) && (hostComboBox.getActionListeners().length == 0)) { + hostComboBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + try { + if((hostCheckBox != null) && hostCheckBox.isSelected()) { + populateTimingMetricPanel(); + } + } catch (HealthMonitorException ex) { + logger.log(Level.SEVERE, "Error populating timing metric panel", ex); + } + } + }); + } + + // Set up a listener on the host name check box + if((hostCheckBox != null) && (hostCheckBox.getActionListeners().length == 0)) { + hostCheckBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + try { + hostComboBox.setEnabled(hostCheckBox.isSelected()); // Why isn't this working? + populateTimingMetricPanel(); + } catch (HealthMonitorException ex) { + logger.log(Level.SEVERE, "Error populating timing metric panel", ex); + } + } + }); + } } /** @@ -131,38 +218,52 @@ public class HealthMonitorDashboard { } private void populateTimingMetricPanel() throws HealthMonitorException { - initializeTimingButtonPanel(); + initializeTimingMetricPanel(); - - // Get a fresh copy of the timing data from the database - Map> timingData = ServicesHealthMonitor.getInstance().getTimingMetricsFromDatabase(); + + // Add title + JLabel timingMetricTitle = new JLabel("Timing Metrics"); + timingMetricTitle.setFont(new Font("Serif", Font.BOLD, 20)); + timingMetricPanel.add(timingMetricTitle); // Add the button controls timingMetricPanel.add(timingButtonPanel); + timingMetricPanel.add(new JSeparator()); for(String name:timingData.keySet()) { // Add the metric name - JLabel label = new JLabel(name); - timingMetricPanel.add(label); + JLabel metricNameLabel = new JLabel(name); + metricNameLabel.setFont(new Font("Serif", Font.BOLD, 12)); + timingMetricPanel.add(metricNameLabel); // If necessary, trim down the list of results to fit the selected time range - List timingDataForDisplay; + List intermediateTimingDataForDisplay; if(dateComboBox.getSelectedItem() != null) { DateRange selectedDateRange = DateRange.fromLabel(dateComboBox.getSelectedItem().toString()); if(selectedDateRange != DateRange.ALL) { long threshold = System.currentTimeMillis() - selectedDateRange.getTimestampRange(); - timingDataForDisplay = timingData.get(name).stream() + intermediateTimingDataForDisplay = timingData.get(name).stream() .filter(t -> t.getTimestamp() > threshold) .collect(Collectors.toList()); } else { - timingDataForDisplay = timingData.get(name); + intermediateTimingDataForDisplay = timingData.get(name); } } else { - timingDataForDisplay = timingData.get(name); + intermediateTimingDataForDisplay = timingData.get(name); + } + + // If necessary, trim down the resulting list to only one host name + List timingDataForDisplay; + if(hostCheckBox.isSelected() && (hostComboBox.getSelectedItem() != null)) { + timingDataForDisplay = intermediateTimingDataForDisplay.stream() + .filter(t -> t.getHostName().equals(hostComboBox.getSelectedItem().toString())) + .collect(Collectors.toList()); + } else { + timingDataForDisplay = intermediateTimingDataForDisplay; } TimingMetricGraphPanel singleTimingGraphPanel = new TimingMetricGraphPanel(timingDataForDisplay, TimingMetricGraphPanel.TimingMetricType.AVERAGE, true); - singleTimingGraphPanel.setPreferredSize(new Dimension(800,200)); + singleTimingGraphPanel.setPreferredSize(new Dimension(900,250)); timingMetricPanel.add(singleTimingGraphPanel); } diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/Installer.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/Installer.java index 5d294aca31..61ea5a5244 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/Installer.java @@ -26,7 +26,6 @@ import org.sleuthkit.autopsy.coreutils.Logger; public class Installer extends ModuleInstall { private static final Logger logger = Logger.getLogger(Installer.class.getName()); - private final HealthMonitorCaseEventListener pcl = new HealthMonitorCaseEventListener(); private static final long serialVersionUID = 1L; private static Installer instance; @@ -45,10 +44,8 @@ public class Installer extends ModuleInstall { @Override public void restored() { - Case.addPropertyChangeListener(pcl); - try { - ServicesHealthMonitor.startUp(); + EnterpriseHealthMonitor.startUpIfEnabled(); } catch (HealthMonitorException ex) { logger.log(Level.SEVERE, "Error starting health services monitor", ex); } diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/TestPanel.form b/Core/src/org/sleuthkit/autopsy/healthmonitor/TestPanel.form index 62e0a5eb17..2ed5b4a0cc 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/TestPanel.form +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/TestPanel.form @@ -73,7 +73,7 @@ - + @@ -84,7 +84,10 @@ - + + + + @@ -132,7 +135,9 @@ - + + + @@ -334,5 +339,15 @@
+ + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/TestPanel.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/TestPanel.java index bca5a729d8..c657bba12f 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/TestPanel.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/TestPanel.java @@ -18,7 +18,7 @@ import javax.swing.JScrollPane; import javax.swing.BorderFactory; import java.util.Map; import javax.swing.BoxLayout; -import org.sleuthkit.autopsy.healthmonitor.ServicesHealthMonitor.DatabaseTimingResult; // TEMP TEMP +import org.sleuthkit.autopsy.healthmonitor.EnterpriseHealthMonitor.DatabaseTimingResult; // TEMP TEMP /** * This is for testing the Health Monitor code @@ -62,6 +62,7 @@ public class TestPanel extends javax.swing.JDialog { nNodesTextField = new javax.swing.JTextField(); jButton2 = new javax.swing.JButton(); newGraphButton = new javax.swing.JButton(); + jButton3 = new javax.swing.JButton(); setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); @@ -175,6 +176,13 @@ public class TestPanel extends javax.swing.JDialog { } }); + org.openide.awt.Mnemonics.setLocalizedText(jButton3, org.openide.util.NbBundle.getMessage(TestPanel.class, "TestPanel.jButton3.text")); // NOI18N + jButton3.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + jButton3ActionPerformed(evt); + } + }); + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); getContentPane().setLayout(layout); layout.setHorizontalGroup( @@ -227,7 +235,9 @@ public class TestPanel extends javax.swing.JDialog { .addComponent(jLabel5, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(nNodesTextField)) .addGap(18, 18, 18) - .addComponent(jButton2) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jButton3) + .addComponent(jButton2)) .addGap(0, 0, Short.MAX_VALUE)))) ); layout.setVerticalGroup( @@ -264,7 +274,9 @@ public class TestPanel extends javax.swing.JDialog { .addComponent(nDaysTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(nNodesTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(jButton2)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 66, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jButton3) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 37, Short.MAX_VALUE) .addComponent(textExistButton) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -300,18 +312,18 @@ public class TestPanel extends javax.swing.JDialog { if(nameTextField.getText().isEmpty() || durationTextField.getText().isEmpty()) { return; } - TimingMetric m = ServicesHealthMonitor.getTimingMetric(nameTextField.getText()); + TimingMetric m = EnterpriseHealthMonitor.getTimingMetric(nameTextField.getText()); try { Thread.sleep(Long.parseLong(durationTextField.getText())); } catch (Exception ex) { ex.printStackTrace(); } - ServicesHealthMonitor.submitTimingMetric(m); + EnterpriseHealthMonitor.submitTimingMetric(m); }//GEN-LAST:event_submitMetricButtonActionPerformed private void enabledCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_enabledCheckBoxActionPerformed try { - ServicesHealthMonitor.setEnabled(enabledCheckBox.isSelected()); + EnterpriseHealthMonitor.setEnabled(enabledCheckBox.isSelected()); } catch (HealthMonitorException ex) { ex.printStackTrace(); } @@ -357,7 +369,7 @@ public class TestPanel extends javax.swing.JDialog { // TEMP TEMP try { - Map> timingData = ServicesHealthMonitor.getInstance().getTimingMetricsFromDatabase(); + Map> timingData = EnterpriseHealthMonitor.getInstance().getTimingMetricsFromDatabase(); String[] dateOptionStrings = {"All", "Two weeks", "One week"}; JComboBox dateComboBox = new JComboBox(dateOptionStrings); @@ -422,7 +434,7 @@ public class TestPanel extends javax.swing.JDialog { try { int nDays = Integer.valueOf(nDaysTextField.getText()); int nNodes = Integer.valueOf(nNodesTextField.getText()); - ServicesHealthMonitor.getInstance().populateDatabase(nDays, nNodes); // TEMP TEMP + EnterpriseHealthMonitor.getInstance().populateDatabase(nDays, nNodes, false); // TEMP TEMP } catch (Exception ex) { ex.printStackTrace(); } @@ -438,6 +450,21 @@ public class TestPanel extends javax.swing.JDialog { } }//GEN-LAST:event_newGraphButtonActionPerformed + private void jButton3ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton3ActionPerformed + if(nDaysTextField.getText().isEmpty() || nNodesTextField.getText().isEmpty()) { + System.out.println("Missing fields"); + return; + } + + try { + int nDays = Integer.valueOf(nDaysTextField.getText()); + int nNodes = Integer.valueOf(nNodesTextField.getText()); + EnterpriseHealthMonitor.getInstance().populateDatabase(nDays, nNodes, true); // TEMP TEMP + } catch (Exception ex) { + ex.printStackTrace(); + } + }//GEN-LAST:event_jButton3ActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton closeButton; private javax.swing.JButton deleteButton; @@ -446,6 +473,7 @@ public class TestPanel extends javax.swing.JDialog { private javax.swing.JButton graphButton; private javax.swing.JButton jButton1; private javax.swing.JButton jButton2; + private javax.swing.JButton jButton3; private javax.swing.JLabel jLabel1; private javax.swing.JLabel jLabel2; private javax.swing.JLabel jLabel3; diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/TimingMetricGraphPanel.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/TimingMetricGraphPanel.java index c83497aa5c..487d059405 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/TimingMetricGraphPanel.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/TimingMetricGraphPanel.java @@ -18,10 +18,8 @@ */ package org.sleuthkit.autopsy.healthmonitor; -import com.mchange.v2.cfg.DelayedLogItem; import java.awt.BasicStroke; import java.awt.Color; -import java.awt.Dimension; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; @@ -37,24 +35,23 @@ import java.util.GregorianCalendar; import javax.swing.JPanel; import org.sleuthkit.autopsy.coreutils.Logger; import java.util.logging.Level; -import org.sleuthkit.autopsy.healthmonitor.ServicesHealthMonitor.DatabaseTimingResult; +import java.util.TimeZone; +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 int width = 800; - private int heigth = 400; private int padding = 25; private int labelPadding = 25; private Color lineColor = new Color(0x12, 0x20, 0xdb, 180); - private Color pointColor = new Color(100, 100, 100, 180); private Color gridColor = new Color(200, 200, 200, 200); private 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 int pointWidth = 4; private int numberYDivisions = 10; private List timingResults; @@ -76,21 +73,10 @@ class TimingMetricGraphPanel extends JPanel { } } - /*void loadNewData(List timingResults, TimingMetricType timingMetricType, boolean doLineGraph) { - this.timingResults = timingResults; - this.timingMetricType = timingMetricType; - this.doLineGraph = doLineGraph; - try { - trendLine = new TrendLine(timingResults, timingMetricType); - } 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; - } - this.revalidate(); - this.repaint(); - }*/ - + /** + * Get the highest metric time for the given type + * @return the highest metric time + */ private double getMaxMetricTime() { // Find the highest of the values being graphed double maxScore = Double.MIN_VALUE; @@ -112,6 +98,10 @@ class TimingMetricGraphPanel extends JPanel { return maxScore; } + /** + * Get the lowest metric time for the given type + * @return the lowest metric time + */ private double getMinMetricTime() { // Find the lowest of the values being graphed double minScore = Double.MAX_VALUE; @@ -133,6 +123,10 @@ class TimingMetricGraphPanel extends JPanel { return minScore; } + /** + * Get the largest timestamp in the data collection + * @return the largest timestamp + */ private long getMaxTimestamp() { long maxTimestamp = Long.MIN_VALUE; for (DatabaseTimingResult score : timingResults) { @@ -141,6 +135,10 @@ class TimingMetricGraphPanel extends JPanel { return maxTimestamp; } + /** + * Get the smallest timestamp in the data collection + * @return the minimum timestamp + */ private long getMinTimestamp() { long minTimestamp = Long.MAX_VALUE; for (DatabaseTimingResult score : timingResults) { @@ -150,7 +148,14 @@ class TimingMetricGraphPanel extends JPanel { } /** + * 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 */ @Override @@ -159,43 +164,69 @@ class TimingMetricGraphPanel extends JPanel { Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - // Get the max and min timestamps and convert to days to create the x-axis + // 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. long originalMaxTimestamp = getMaxTimestamp(); - double maxTimestamp = (double) originalMaxTimestamp + 1000 * 60 *60 * 4; // Four hour buffer - double minTimestamp = getMinTimestamp() - 1000 * 60 * 60 * 4; // Four hour buffer - System.out.println("originalMax: " + originalMaxTimestamp + " new max: " + maxTimestamp + " new min: " + minTimestamp); + double maxValueOnXAxis = (double) originalMaxTimestamp + 1000 * 60 *60 * 2; // Two hour buffer + double minValueOnXAxis = getMinTimestamp() - 1000 * 60 * 60 * 2; // Two hour buffer // Get the max and min times to create the y-axis - double maxMetricTime = getMaxMetricTime(); - double minMetricTime = getMinMetricTime(); - minMetricTime = Math.max(0, minMetricTime - (maxMetricTime * 0.1)); - maxMetricTime = maxMetricTime * 1.1; + // We add a small buffer to each side so the data won't overwrite the axes. + double maxValueOnYAxis = getMaxMetricTime(); + double minValueOnYAxis = getMinMetricTime(); + minValueOnYAxis = Math.max(0, minValueOnYAxis - (maxValueOnYAxis * 0.1)); + maxValueOnYAxis = maxValueOnYAxis * 1.1; - double xScale = ((double) getWidth() - (2 * padding) - labelPadding) / (maxTimestamp - minTimestamp); - double yScale = ((double) getHeight() - (2 * padding) - labelPadding) / (maxMetricTime - minMetricTime); + // The graph itself has the following corners: + // (padding + label padding, padding) - top left + // (padding + label padding, getHeight() - label padding - padding x 2) - bottom left + // (getWidth() - padding, getHeight() - label padding - padding x 2) - top right ?? + // (padding + label padding, getHeight() - label padding - padding x 2) - bottom right + int leftGraphPadding = padding + labelPadding; + int rightGraphPadding = padding; + int topGraphPadding = padding; + int bottomGraphPadding = padding + 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); - System.out.println("xScale: " + xScale + ", yScale: " + yScale); - // draw white background g2.setColor(Color.WHITE); - g2.fillRect(padding + labelPadding, padding, getWidth() - (2 * padding) - labelPadding, getHeight() - 2 * padding - labelPadding); - g2.setColor(Color.BLACK); + g2.fillRect(leftGraphPadding, topGraphPadding, graphWidth, graphHeight); // create hatch marks and grid lines for y axis. for (int i = 0; i < numberYDivisions + 1; i++) { - int x0 = padding + labelPadding; - int x1 = pointWidth + padding + labelPadding; - int y0 = getHeight() - ((i * (getHeight() - padding * 2 - labelPadding)) / numberYDivisions + padding + labelPadding); + int x0 = leftGraphPadding; + int x1 = pointWidth + leftGraphPadding; + int y0 = getHeight() - ((i * graphHeight) / numberYDivisions + bottomGraphPadding); int y1 = y0; + if (timingResults.size() > 0) { + // Draw the grid line g2.setColor(gridColor); - g2.drawLine(padding + labelPadding + 1 + pointWidth, y0, getWidth() - padding, y1); + g2.drawLine(leftGraphPadding + 1 + pointWidth, y0, getWidth() - rightGraphPadding, y1); + + // Create the label g2.setColor(Color.BLACK); - String yLabel = ((int) ((getMinMetricTime() + (maxMetricTime - minMetricTime) * ((i * 1.0) / numberYDivisions)) * 100)) / 100.0 + ""; - FontMetrics metrics = g2.getFontMetrics(); - int labelWidth = metrics.stringWidth(yLabel); - g2.drawString(yLabel, x0 - labelWidth - 5, y0 + (metrics.getHeight() / 2) - 3); + double yValue = minValueOnYAxis + ((maxValueOnYAxis - minValueOnYAxis) * ((i * 1.0) / numberYDivisions)); + String yLabel = ((int) (yValue * 100)) / 100.0 + ""; + FontMetrics fontMetrics = g2.getFontMetrics(); + int labelWidth = fontMetrics.stringWidth(yLabel); + g2.drawString(yLabel, x0 - labelWidth - 5, y0 + (fontMetrics.getHeight() / 2) - 3); } + + // Draw the small hatch mark + g2.setColor(Color.BLACK); g2.drawLine(x0, y0, x1, y1); } @@ -207,11 +238,10 @@ class TimingMetricGraphPanel extends JPanel { maxDate.set(Calendar.SECOND, 0); maxDate.set(Calendar.MILLISECOND, 0); long maxMidnightInMillis = maxDate.getTimeInMillis(); - System.out.println("Last timestamp: " + originalMaxTimestamp + ", last midnight: " + maxMidnightInMillis); - // We don't want to display more than 20 grid lines - long totalDays = (maxMidnightInMillis - getMinTimestamp()) / MILLISECONDS_PER_DAY; - System.out.println(" Total days: " + totalDays); + // 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; @@ -223,16 +253,19 @@ class TimingMetricGraphPanel extends JPanel { } // Draw the vertical grid lines and labels - for (long currentDivision = maxMidnightInMillis; currentDivision >= minTimestamp; currentDivision -= MILLISECONDS_PER_DAY * daysPerDivision) { + // 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) ((double)(currentDivision - minTimestamp) * xScale + padding + labelPadding); + //long currentDivision = + int x0 = (int) ((double)(currentDivision - minValueOnXAxis) * xScale + leftGraphPadding); int x1 = x0; - int y0 = getHeight() - padding - labelPadding; + int y0 = getHeight() - bottomGraphPadding; int y1 = y0 - pointWidth; // Draw the light grey grid line g2.setColor(gridColor); - g2.drawLine(x0, getHeight() - padding - labelPadding - 1 - pointWidth, x1, padding); + g2.drawLine(x0, getHeight() - bottomGraphPadding - 1 - pointWidth, x1, topGraphPadding); // Draw the hatch mark g2.setColor(Color.BLACK); @@ -240,21 +273,23 @@ class TimingMetricGraphPanel extends JPanel { // Draw the label Calendar thisDate = new GregorianCalendar(); + thisDate.setTimeZone(TimeZone.getTimeZone("GMT")); 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(); int labelWidth = metrics.stringWidth(xLabel); g2.drawString(xLabel, x0 - labelWidth / 2, y0 + metrics.getHeight() + 3); } - // create x and y axes - g2.drawLine(padding + labelPadding, getHeight() - padding - labelPadding, padding + labelPadding, padding); - g2.drawLine(padding + labelPadding, getHeight() - padding - labelPadding, getWidth() - padding, getHeight() - padding - labelPadding); + // 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); - // Plot the timing points - g2.setStroke(GRAPH_STROKE); + // Create the points to plot List graphPoints = new ArrayList<>(); for (int i = 0; i < timingResults.size(); i++) { double metricTime; @@ -272,9 +307,9 @@ class TimingMetricGraphPanel extends JPanel { } - int x1 = (int) ((double)(maxTimestamp - timingResults.get(i).getTimestamp()) * xScale + padding + labelPadding); - int yAve = (int) ((maxMetricTime - metricTime) * yScale + padding); - graphPoints.add(new Point(x1, yAve)); + 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 @@ -289,14 +324,12 @@ class TimingMetricGraphPanel extends JPanel { return 0; } }); - - System.out.println("points: "); - for(Point p:graphPoints){ - System.out.println(p.getX() + ", " + p.getY()); - } + // Draw the selected type of graph. If there's only one data point, + // draw it. + g2.setStroke(NARROW_STROKE); g2.setColor(lineColor); - if(doLineGraph) { + 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; @@ -315,20 +348,99 @@ class TimingMetricGraphPanel extends JPanel { } // Draw the trend line - int x0 = (int) ((double)(maxTimestamp - minTimestamp) * xScale + padding + labelPadding); - int y0 = (int) ((double)(maxMetricTime - trendLine.getExpectedValueAt(minTimestamp)) * yScale + padding); - int x1 = (int) ((double)(0) * xScale + padding + labelPadding); - int y1 = (int) ((double)(maxMetricTime - trendLine.getExpectedValueAt(maxTimestamp)) * yScale + padding); - g2.setColor(trendLineColor); - g2.drawLine(x0, y0, x1, y1); - - // TODO - temp testing where origin is - g2.fillOval(0, 0, 20, 20); + // Don't draw anything if there's only one data point + if(trendLine != null && (timingResults.size() > 1)) { + int x0 = (int) ((double)(0) * xScale + padding + labelPadding); + int y0 = (int) ((double)(maxValueOnYAxis - trendLine.getExpectedValueAt(minValueOnXAxis)) * yScale + padding); + int x1 = (int) ((double)(maxValueOnXAxis - minValueOnXAxis) * xScale + padding + labelPadding); + int y1 = (int) ((double)(maxValueOnYAxis - trendLine.getExpectedValueAt(maxValueOnXAxis)) * yScale + padding); + g2.setStroke(GRAPH_STROKE); + g2.setColor(trendLineColor); + g2.drawLine(x0, y0, x1, y1); + } } + /** + * The metric field we want to graph + */ enum TimingMetricType { AVERAGE, MAX, MIN; } + + /** + * 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 + */ + class TrendLine { + + double slope; + double yInt; + + TrendLine(List timingResults, TimingMetricGraphPanel.TimingMetricType timingMetricType) 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; + switch (timingMetricType) { + case MAX: + y = timingResults.get(i).getMax(); + break; + case MIN: + y = timingResults.get(i).getMin(); + break; + case AVERAGE: + default: + y = timingResults.get(i).getAverage(); + break; + } + + 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); + } + } + } diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/TrendLine.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/TrendLine.java deleted file mode 100644 index 3cabc2cdae..0000000000 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/TrendLine.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2018 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.healthmonitor; - -import java.util.List; -import org.sleuthkit.autopsy.healthmonitor.ServicesHealthMonitor.DatabaseTimingResult; - -/** - * - */ -class TrendLine { - - double slope; - double yInt; - - TrendLine(List timingResults, TimingMetricGraphPanel.TimingMetricType timingMetricType) 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; - switch (timingMetricType) { - case MAX: - y = timingResults.get(i).getMax(); - break; - case MIN: - y = timingResults.get(i).getMin(); - break; - case AVERAGE: - default: - y = timingResults.get(i).getAverage(); - break; - } - - sumX += x; - sumY += y; - sumXY += x * y; - sumXsquared += x * x; - } - - // Calculate slope - slope = (n * sumXY - sumX * sumY) / (n * sumXsquared - sumX * sumX); - - // Calculate y intercept - yInt = (sumY - slope * sumX) / n; - - System.out.println("Trend line: y = " + slope + " * x + " + yInt); - } - - double getExpectedValueAt(double x) { - return (slope * x + yInt); - } -} diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestTasksScheduler.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestTasksScheduler.java index 7bbee3a309..26e603e55b 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestTasksScheduler.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestTasksScheduler.java @@ -410,11 +410,18 @@ final class IngestTasksScheduler { } /* - * Check if the file is a member of the file ingest filter that is being - * applied to the current run of ingest, checks if unallocated space - * should be processed inside call to fileIsMemberOf + * Ensures that all directories, files which are members of the ingest + * file filter, and unallocated blocks (when processUnallocated is + * enabled) all continue to be processed. AbstractFiles which do not + * meet one of these criteria will be skipped. + * + * An additional check to see if unallocated space should be processed + * is part of the FilesSet.fileIsMemberOf() method. + * + * This code may need to be updated when + * TSK_DB_FILES_TYPE_ENUM.UNUSED_BLOCKS comes into use by Autopsy. */ - if (!file.isDir() && task.getIngestJob().getFileIngestFilter().fileIsMemberOf(file) == null) { + if (!file.isDir() && !shouldBeCarved(task) && !fileAcceptedByFilter(task)) { return false; } @@ -462,6 +469,30 @@ final class IngestTasksScheduler { return true; } + /** + * Check whether or not a file should be carved for a data source ingest + * ingest job. + * + * @param task The file ingest task for the file. + * + * @return True or false. + */ + private static boolean shouldBeCarved(final FileIngestTask task) { + return task.getIngestJob().shouldProcessUnallocatedSpace() && task.getFile().getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS); + } + + /** + * Checks whether or not a file is accepted (passes) the file filter for a data + * source ingest job. + * + * @param task The file ingest task for the file. + * + * @return True or false. + */ + private static boolean fileAcceptedByFilter(final FileIngestTask task) { + return !(task.getIngestJob().getFileIngestFilter().fileIsMemberOf(task.getFile()) == null); + } + /** * Checks whether or not a collection of ingest tasks includes a task for a * given data source ingest job. diff --git a/Core/test/filter_test1.img b/Core/test/filter_test1.img deleted file mode 100755 index 69b6e21b6c..0000000000 Binary files a/Core/test/filter_test1.img and /dev/null differ diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/ingest/IngestFileFiltersTest.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/ingest/IngestFileFiltersTest.java index 7caab79205..54b7024790 100644 --- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/ingest/IngestFileFiltersTest.java +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/ingest/IngestFileFiltersTest.java @@ -33,6 +33,7 @@ import org.sleuthkit.autopsy.casemodule.CaseActionException; import org.sleuthkit.autopsy.casemodule.CaseDetails; import junit.framework.Test; import org.apache.commons.io.FileUtils; +import org.netbeans.junit.NbTestCase; import org.openide.util.Exceptions; import org.python.icu.impl.Assert; import org.sleuthkit.autopsy.casemodule.ImageDSProcessor; @@ -53,11 +54,11 @@ import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; -public class IngestFileFiltersTest extends TestCase { +public class IngestFileFiltersTest extends NbTestCase { private static final Path CASE_DIRECTORY_PATH = Paths.get(System.getProperty("java.io.tmpdir"), "IngestFileFiltersTest"); private static final File CASE_DIR = new File(CASE_DIRECTORY_PATH.toString()); - private static final Path IMAGE_PATH = Paths.get("test/filter_test1.img"); + private final Path IMAGE_PATH = Paths.get(this.getDataDir().toString(),"filter_test1.img"); public static Test suite() { NbModuleSuite.Configuration conf = NbModuleSuite.createConfiguration(IngestFileFiltersTest.class). @@ -66,6 +67,10 @@ public class IngestFileFiltersTest extends TestCase { return conf.suite(); } + public IngestFileFiltersTest(String name) { + super(name); + } + @Override public void setUp() { // Delete the test directory, if it exists @@ -88,7 +93,7 @@ public class IngestFileFiltersTest extends TestCase { } catch (CaseActionException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex); - } + } assertTrue(CASE_DIR.exists()); ImageDSProcessor dataSourceProcessor = new ImageDSProcessor(); try { diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/AddMemoryImageTask.java b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/AddMemoryImageTask.java index 57d14452b5..7490c3e7b1 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/AddMemoryImageTask.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/AddMemoryImageTask.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2011-2016 Basis Technology Corp. + * + * Copyright 2018 Basis Technology Corp. * Contact: carrier sleuthkit org - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -23,80 +23,89 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.logging.Level; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; -import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.Image; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.Content; /* - * A runnable that adds a raw data source to a case database. + * A runnable that adds a memory image data source to a case database. */ final class AddMemoryImageTask implements Runnable { - private static final Logger logger = Logger.getLogger(AddMemoryImageTask.class.getName()); + private final static Logger logger = Logger.getLogger(AddMemoryImageTask.class.getName()); private final String deviceId; - private final String imageFilePath; + private final String memoryImagePath; private final String timeZone; - private final List pluginsToRun; + private final List pluginsToRun; private final DataSourceProcessorProgressMonitor progressMonitor; private final DataSourceProcessorCallback callback; - private VolatilityProcessor volatilityProcessor = null; - private boolean isCancelled = false; - + private volatile VolatilityProcessor volatilityProcessor; + private volatile boolean isCancelled; + /** - * Constructs a runnable that adds a raw data source to a case database. + * Constructs a runnable that adds a memory image to a case database. * - * @param deviceId An ASCII-printable identifier for the - * device associated with the data source - * that is intended to be unique across - * multiple cases (e.g., a UUID). - * @param imageFilePath Path to a Raw data source file. - * @param timeZone The time zone to use when processing dates - * and times for the image, obtained from - * java.util.TimeZone.getID. - * @param breakupChunks 2GB or not breakup. - * @param progressMonitor Progress monitor for reporting - * progressMonitor during processing. - * @param callback Callback to call when processing is done. + * @param deviceId An ASCII-printable identifier for the device + * associated with the data source that is intended + * to be unique across multiple cases (e.g., a UUID). + * @param memoryImagePath Path to the memory image file. + * @param pluginsToRun The Volatility plugins to run. + * @param timeZone The time zone to use when processing dates and + * times for the image, obtained from + * java.util.TimeZone.getID. + * @param progressMonitor Progress monitor for reporting progressMonitor + * during processing. + * @param callback Callback to call when processing is done. */ - AddMemoryImageTask(String deviceId, String imageFilePath, List PluginsToRun, String timeZone, long chunkSize, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { + AddMemoryImageTask(String deviceId, String memoryImagePath, List pluginsToRun, String timeZone, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { this.deviceId = deviceId; - this.imageFilePath = imageFilePath; - this.pluginsToRun = PluginsToRun; + this.memoryImagePath = memoryImagePath; + this.pluginsToRun = pluginsToRun; this.timeZone = timeZone; this.callback = callback; this.progressMonitor = progressMonitor; } /** - * Adds a raw data source to a case database. + * Adds a memory image data source to a case database. */ - @Override + @Messages({ + "# {0} - exception message", + "AddMemoryImageTask_errorMessage_criticalException= Critical error: {0}", + }) + @Override public void run() { + if (isCancelled) { + return; + } progressMonitor.setIndeterminate(true); progressMonitor.setProgress(0); + List dataSources = new ArrayList<>(); List errorMessages = new ArrayList<>(); boolean criticalErrorOccurred = false; - Image dataSource = addImageToCase(errorMessages); - if (dataSource == null) { + try { + Image dataSource = addImageToCase(); + dataSources.add(dataSource); + volatilityProcessor = new VolatilityProcessor(memoryImagePath, dataSource, pluginsToRun, progressMonitor); + volatilityProcessor.run(); + } catch (NoCurrentCaseException | TskCoreException | VolatilityProcessor.VolatilityProcessorException ex) { criticalErrorOccurred = true; + errorMessages.add(Bundle.AddMemoryImageTask_errorMessage_criticalException(ex.getLocalizedMessage())); + /* + * Log the exception as well as add it to the error messages, to + * ensure that the stack trace is not lost. + */ + logger.log(Level.SEVERE, String.format("Critical error processing memory image data source at %s for device %s", memoryImagePath, deviceId), ex); } - /* call Volatility to process the image */ - else { - if (isCancelled) - return; - - volatilityProcessor = new VolatilityProcessor(imageFilePath, dataSource, pluginsToRun, progressMonitor); - if (volatilityProcessor.run()) { - criticalErrorOccurred = true; - } - errorMessages.addAll(volatilityProcessor.getErrorMessages()); - } - + errorMessages.addAll(volatilityProcessor.getErrorMessages()); progressMonitor.setProgress(100); /** @@ -110,55 +119,64 @@ final class AddMemoryImageTask implements Runnable { } else { result = DataSourceProcessorCallback.DataSourceProcessorResult.NO_ERRORS; } - - callback.done(result, errorMessages, new ArrayList<>(Arrays.asList(dataSource))); + callback.done(result, errorMessages, dataSources); } /** - * Attempts to add the input image to the case. + * Attempts to add the input memory image to the case as a data source. * - * @param errorMessages If there are any error messages, the error messages - * are added to this list for eventual return to the - * caller via the callback. - * @returns Image that was added to DB or null on error + * @return The Image object representation of the memeory image file data + * source. + * + * @throws NoCurrentCaseException If there is no current case. + * @throws TskCoreException If there is an error adding the data + * source to the case database. */ - @Messages({"AddMemoryImageTask.progress.add.text=Adding memory image: ", - "AddMemoryImageTask.image.critical.error.adding=Critical error adding ", - "AddMemoryImageTask.for.device=for device ", - "AddMemoryImageTask.image.notExisting=is not existing.", - "AddMemoryImageTask.image.noncritical.error.adding=Non-critical error adding "}) - private Image addImageToCase(List errorMessages) { - progressMonitor.setProgressText(Bundle.AddMemoryImageTask_progress_add_text() + imageFilePath); - - SleuthkitCase caseDatabase = Case.getCurrentCase().getSleuthkitCase(); - caseDatabase.acquireExclusiveLock(); + @Messages({ + "# {0} - image file path", + "AddMemoryImageTask_progressMessage_addingImageFile= Adding memory image {0}", + "# {0} - image file path", + "# {1} - device id", + "AddMemoryImageTask_exceptionMessage_noImageFile= Memory image file {0} for device {1} does not exist" + }) + private Image addImageToCase() throws NoCurrentCaseException, TskCoreException { + progressMonitor.setProgressText(Bundle.AddMemoryImageTask_progressMessage_addingImageFile( memoryImagePath)); - // verify it exists - File imageFile = Paths.get(imageFilePath).toFile(); - if (!imageFile.exists()) { - errorMessages.add(Bundle.AddMemoryImageTask_image_critical_error_adding() + imageFilePath + Bundle.AddMemoryImageTask_for_device() - + deviceId + Bundle.AddMemoryImageTask_image_notExisting()); - return null; - } - + SleuthkitCase caseDatabase = Case.getOpenCase().getSleuthkitCase(); + caseDatabase.acquireSingleUserCaseWriteLock(); try { - // add it to the DB - List imageFilePaths = new ArrayList<>(); - imageFilePaths.add(imageFilePath); - Image dataSource = caseDatabase.addImageInfo(0, imageFilePaths, timeZone); //TODO: change hard coded deviceId. + /* + * Verify the memory image file exists. + */ + File imageFile = Paths.get(memoryImagePath).toFile(); + if (!imageFile.exists()) { + throw new TskCoreException(Bundle.AddMemoryImageTask_exceptionMessage_noImageFile(memoryImagePath, deviceId)); + } + + /* + * Add the data source. + * + * NOTE: The object id for device passed to + * SleuthkitCase.addImageInfo is hard-coded to zero for now. This + * will need to be changed when a Device abstraction is added to the + * SleuthKit data model. + */ + Image dataSource = caseDatabase.addImageInfo(0, new ArrayList<>(Arrays.asList(memoryImagePath)), timeZone); return dataSource; - } catch (TskCoreException ex) { - errorMessages.add(Bundle.AddMemoryImageTask_image_critical_error_adding() + imageFilePath + Bundle.AddMemoryImageTask_for_device() + deviceId + ":" + ex.getLocalizedMessage()); - return null; + } finally { - caseDatabase.releaseExclusiveLock(); - } + caseDatabase.releaseSingleUserCaseWriteLock(); + } } + /** + * Requests cancellation of this task by setting a cancelled flag. + */ void cancelTask() { isCancelled = true; if (volatilityProcessor != null) { volatilityProcessor.cancel(); } } + } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSInputPanel.java b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSInputPanel.java index b2a893964a..f96a9cc801 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSInputPanel.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSInputPanel.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2011-2016 Basis Technology Corp. + * + * Copyright 2018 Basis Technology Corp. * Contact: carrier sleuthkit org - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -37,11 +37,13 @@ import javax.swing.table.AbstractTableModel; import javax.swing.table.TableColumn; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; import org.sleuthkit.autopsy.coreutils.ModuleSettings; import org.sleuthkit.autopsy.coreutils.PathValidator; final class MemoryDSInputPanel extends JPanel implements DocumentListener { + private static final long serialVersionUID = 1L; //default private final String PROP_LASTINPUT_PATH = "LBL_LastInputFile_PATH"; private final JFileChooser fc = new JFileChooser(); @@ -52,14 +54,14 @@ final class MemoryDSInputPanel extends JPanel implements DocumentListener { private final List PluginListNames = new ArrayList<>(); private final Map pluginListStates = new HashMap<>(); // is set by listeners when users select and deselect items private final Boolean isEnabled = true; - + /** - * Creates new MemoryDSInputPanel panel for user input + * Creates new MemoryDSInputPanel panel for user input */ private MemoryDSInputPanel(String context) { - this.pluginList = new String[]{"amcache","cmdline","cmdscan","consoles","malfind","netscan","notepad","pslist","psxview","shellbags","shimcache","shutdown","userassist", "apihooks","connscan","devicetree","dlllist","envars","filescan","gahti","getservicesids","getsids","handles","hashdump","hivelist","hivescan","impscan","ldrmodules","lsadump","modules","mutantscan","privs","psscan","pstree","sockets","svcscan","shimcache","timeliner","unloadedmodules","userhandles","vadinfo","verinfo"}; + this.pluginList = new String[]{"amcache", "cmdline", "cmdscan", "consoles", "malfind", "netscan", "notepad", "pslist", "psxview", "shellbags", "shimcache", "shutdown", "userassist", "apihooks", "connscan", "devicetree", "dlllist", "envars", "filescan", "gahti", "getservicesids", "getsids", "handles", "hashdump", "hivelist", "hivescan", "impscan", "ldrmodules", "lsadump", "modules", "mutantscan", "privs", "psscan", "pstree", "sockets", "svcscan", "shimcache", "timeliner", "unloadedmodules", "userhandles", "vadinfo", "verinfo"}; Arrays.sort(this.pluginList); - + initComponents(); errorLabel.setVisible(false); @@ -91,7 +93,7 @@ final class MemoryDSInputPanel extends JPanel implements DocumentListener { private void postInit() { pathTextField.getDocument().addDocumentListener(this); } - + private void customizePluginListTable() { PluginList.setModel(tableModel); PluginList.setTableHeader(null); @@ -135,14 +137,14 @@ final class MemoryDSInputPanel extends JPanel implements DocumentListener { // set the selected timezone timeZoneComboBox.setSelectedItem(formatted); } - + private void createVolatilityVersionList() { - + volExecutableComboBox.addItem("2.6"); volExecutableComboBox.addItem("2.5"); - + } - + private void createPluginList() { PluginListNames.clear(); pluginListStates.clear(); @@ -150,19 +152,19 @@ final class MemoryDSInputPanel extends JPanel implements DocumentListener { // if the config file doesn't exist, then set them all to enabled boolean allEnabled = !ModuleSettings.configExists(this.contextName); Map pluginMap = ModuleSettings.getConfigSettings(this.contextName); - + for (String plugin : pluginList) { PluginListNames.add(plugin); - if (allEnabled) + if (allEnabled) { pluginListStates.put(plugin, true); - else + } else { pluginListStates.put(plugin, pluginMap.containsKey(plugin)); + } } tableModel.fireTableDataChanged(); //this.tableModel = pluginsToRun.getModel(); } - /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always @@ -282,18 +284,18 @@ final class MemoryDSInputPanel extends JPanel implements DocumentListener { }// //GEN-END:initComponents @SuppressWarnings("deprecation") private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseButtonActionPerformed - String oldText = pathTextField.getText(); - // set the current directory of the FileChooser if the ImagePath Field is valid - File currentDir = new File(oldText); - if (currentDir.exists()) { - fc.setCurrentDirectory(currentDir); - } + String oldText = pathTextField.getText(); + // set the current directory of the FileChooser if the ImagePath Field is valid + File currentDir = new File(oldText); + if (currentDir.exists()) { + fc.setCurrentDirectory(currentDir); + } - int retval = fc.showOpenDialog(this); - if (retval == JFileChooser.APPROVE_OPTION) { - String path = fc.getSelectedFile().getPath(); - pathTextField.setText(path); - } + int retval = fc.showOpenDialog(this); + if (retval == JFileChooser.APPROVE_OPTION) { + String path = fc.getSelectedFile().getPath(); + pathTextField.setText(path); + } }//GEN-LAST:event_browseButtonActionPerformed private void volExecutableComboBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_volExecutableComboBoxActionPerformed @@ -322,7 +324,7 @@ final class MemoryDSInputPanel extends JPanel implements DocumentListener { String getImageFilePath() { return pathTextField.getText(); } - + List getPluginsToRun() { List enabledPlugins = new ArrayList<>(); Map pluginMap = new HashMap<>(); @@ -332,7 +334,7 @@ final class MemoryDSInputPanel extends JPanel implements DocumentListener { pluginMap.put(plugin, ""); } } - + ModuleSettings.setConfigSettings(this.contextName, pluginMap); // @@ Could return keys of set return enabledPlugins; @@ -374,11 +376,19 @@ final class MemoryDSInputPanel extends JPanel implements DocumentListener { * * @param path Absolute path to the selected data source */ - @Messages({"MemoryDSInputPanel.error.text=Path to multi-user data source is on \"C:\" drive"}) + @Messages({ + "MemoryDSInputPanel_errorMsg_noOpenCase=No open case", + "MemoryDSInputPanel_errorMsg_dataSourcePathOnCdrive=Path to multi-user data source is on \"C:\" drive" + }) private void warnIfPathIsInvalid(String path) { - if (!PathValidator.isValid(path, Case.getCurrentCase().getCaseType())) { + try { + if (!PathValidator.isValid(path, Case.getOpenCase().getCaseType())) { + errorLabel.setVisible(true); + errorLabel.setText(Bundle.MemoryDSInputPanel_errorMsg_dataSourcePathOnCdrive()); + } + } catch (NoCurrentCaseException unused) { errorLabel.setVisible(true); - errorLabel.setText(Bundle.MemoryDSInputPanel_error_text()); + errorLabel.setText(Bundle.MemoryDSInputPanel_errorMsg_dataSourcePathOnCdrive()); } } @@ -470,5 +480,4 @@ final class MemoryDSInputPanel extends JPanel implements DocumentListener { } } - } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSProcessor.java b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSProcessor.java index cfe2978fe0..9791ad1f09 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSProcessor.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSProcessor.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2016 Basis Technology Corp. + * Copyright 2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,21 +28,21 @@ import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; /** - * A MEmory data source processor that implements the DataSourceProcessor service - * provider interface to allow integration with the add data source wizard. It - * also provides a run method overload to allow it to be used independently of - * the wizard. + * A memory image data source processor that implements the DataSourceProcessor + * service provider interface to allow integration with the Add Data Source + * wizard. It also provides a run method overload to allow it to be used + * independently of the wizard. */ @ServiceProvider(service = DataSourceProcessor.class) public class MemoryDSProcessor implements DataSourceProcessor { private final MemoryDSInputPanel configPanel; - private AddMemoryImageTask addImageTask = null; + private AddMemoryImageTask addImageTask; /* - * Constructs a Memory data source processor that implements the + * Constructs a memory data source processor that implements the * DataSourceProcessor service provider interface to allow integration with - * the add data source wizard. It also provides a run method overload to + * the Add Data source wizard. It also provides a run method overload to * allow it to be used independently of the wizard. */ public MemoryDSProcessor() { @@ -117,37 +117,40 @@ public class MemoryDSProcessor implements DataSourceProcessor { @Override public void run(DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { configPanel.storeSettings(); - run(UUID.randomUUID().toString(), configPanel.getImageFilePath(), configPanel.getPluginsToRun(), configPanel.getTimeZone(), 0, progressMonitor, callback); + run(UUID.randomUUID().toString(), configPanel.getImageFilePath(), configPanel.getPluginsToRun(), configPanel.getTimeZone(), progressMonitor, callback); } /** - * Adds a "memory" data source to the case database using a background task in - * a separate thread and the given settings instead of those provided by the - * selection and configuration panel. Returns as soon as the background task - * is started and uses the callback object to signal task completion and - * return results. + * Adds a memory image data source to the case database using a background + * task in a separate thread and the given settings instead of those + * provided by the selection and configuration panel. Returns as soon as the + * background task is started and uses the callback object to signal task + * completion and return results. * - * @param deviceId An ASCII-printable identifier for the device - * associated with the data source that is - * intended to be unique across multiple cases - * (e.g., a UUID). - * @param imageFilePath Path to the image file. - * @param timeZone The time zone to use when processing dates - * and times for the image, obtained from - * java.util.TimeZone.getID. - * @param chunkSize The maximum size of each chunk of the raw - * data source as it is divided up into virtual - * unallocated space files. - * @param progressMonitor Progress monitor for reporting progress - * during processing. - * @param callback Callback to call when processing is done. + * @param deviceId An ASCII-printable identifier for the device + * associated with the data source that is intended + * to be unique across multiple cases (e.g., a UUID). + * @param memoryImagePath Path to the memory image file. + * @param pluginsToRun The Volatility plugins to run. + * @param timeZone The time zone to use when processing dates and + * times for the image, obtained from + * java.util.TimeZone.getID. + * @param progressMonitor Progress monitor for reporting progress during + * processing. + * @param callback Callback to call when processing is done. */ - private void run(String deviceId, String imageFilePath, List pluginsToRun, String timeZone, long chunkSize, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { - addImageTask = new AddMemoryImageTask(deviceId, imageFilePath, pluginsToRun, timeZone, 0, progressMonitor, callback); + private void run(String deviceId, String memoryImagePath, List pluginsToRun, String timeZone, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { + addImageTask = new AddMemoryImageTask(deviceId, memoryImagePath, pluginsToRun, timeZone, progressMonitor, callback); new Thread(addImageTask).start(); - //new Thread(new AddLocalFilesTask(deviceId, rootVirtualDirectoryName, localFilePaths, progressMonitor, callback)).start(); } + /** + * Requests cancellation of the background task that adds a data source to + * the case database, after the task is started using the run method. This + * is a "best effort" cancellation, with no guarantees that the case + * database will be unchanged. If cancellation succeeded, the list of new + * data sources returned by the background task will be empty. + */ @Override public void cancel() { if (addImageTask != null) { @@ -165,4 +168,3 @@ public class MemoryDSProcessor implements DataSourceProcessor { } } - diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/VolatilityProcessor.java b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/VolatilityProcessor.java index 31e2e5b8af..50246f5255 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/VolatilityProcessor.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/VolatilityProcessor.java @@ -1,15 +1,15 @@ /* - * Autopsy - * + * Autopsy + * * Copyright 2018 Basis Technology Corp. * Contact: carrier sleuthkit org - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -21,7 +21,6 @@ package org.sleuthkit.autopsy.experimental.volatilityDSP; import java.io.BufferedReader; import java.io.FileReader; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Paths; import java.util.ArrayList; @@ -30,9 +29,10 @@ import java.util.List; import java.util.Set; import java.util.logging.Level; import org.openide.modules.InstalledFileLocator; -import org.openide.util.Exceptions; import org.openide.util.Lookup; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.services.Blackboard; import org.sleuthkit.autopsy.casemodule.services.FileManager; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; @@ -50,159 +50,205 @@ import org.sleuthkit.datamodel.Report; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData.TSK_DB_FILES_TYPE_ENUM; - /** - * Runs Volatility and parses output + * Runs Volatility on a given memory image file and parses the output to create + * artifacts. */ class VolatilityProcessor { - private static final String VOLATILITY_DIRECTORY = "Volatility"; //NON-NLS + + private final static Logger logger = Logger.getLogger(VolatilityProcessor.class.getName()); + private static final String VOLATILITY = "Volatility"; //NON-NLS private static final String VOLATILITY_EXECUTABLE = "volatility_2.6_win64_standalone.exe"; //NON-NLS - private final String memoryImagePath; - private final List pluginsToRun; - private final Image dataSource; - private static final String SEP = System.getProperty("line.separator"); - private static final Logger logger = Logger.getLogger(VolatilityProcessor.class.getName()); - private String moduleOutputPath; - private File executableFile; private final IngestServices services = IngestServices.getInstance(); + private final List errorMsgs = new ArrayList<>(); + private final String memoryImagePath; + private final Image dataSource; + private final List pluginsToRun; private final DataSourceProcessorProgressMonitor progressMonitor; - private boolean isCancelled; + private Case currentCase; + private File executableFile; + private String moduleOutputPath; private FileManager fileManager; - private final List errorMsgs = new ArrayList<>(); + private volatile boolean isCancelled; /** - * - * @param ImagePath String path to memory image file - * @param dataSource Object for memory image that was added to case DB - * @param plugInToRuns list of Volatility plugins to run - * @param progressMonitor DSP progress monitor to report status + * Constructs a processor that runs Volatility on a given memory image file + * and parses the output to create artifacts. + * + * @param memoryImagePath Path to memory image file. + * @param dataSource The memory image data source. + * @param plugInToRuns Volatility plugins to run. + * @param progressMonitor Progress monitor for reporting progress during + * processing. */ - VolatilityProcessor(String ImagePath, Image dataSource, List plugInToRun, DataSourceProcessorProgressMonitor progressMonitor) { - this.memoryImagePath = ImagePath; + VolatilityProcessor(String memoryImagePath, Image dataSource, List plugInToRun, DataSourceProcessorProgressMonitor progressMonitor) { + this.memoryImagePath = memoryImagePath; this.pluginsToRun = plugInToRun; this.dataSource = dataSource; this.progressMonitor = progressMonitor; } - - + /** - * Run volatility and parse the outputs - * @returns true if there was a critical error + * Runs Volatility on a given memory image file and parses the output to + * create artifacts. + * + * @throws VolatilityProcessorException If there is a critical error during + * processing. */ - boolean run() { - executableFile = locateExecutable(); - if (executableFile == null) { - logger.log(Level.SEVERE, "Volatility exe not found"); - return true; + @NbBundle.Messages({ + "VolatilityProcessor_progressMessage_noCurrentCase=Failed to get current case", + "VolatilityProcessor_exceptionMessage_volatilityExeNotFound=Volatility executable not found", + "# {0} - plugin name", + "VolatilityProcessor_progressMessage_runningImageInfo=Running {0} plugin" + }) + void run() throws VolatilityProcessorException { + this.errorMsgs.clear(); + + try { + this.currentCase = Case.getOpenCase(); + } catch (NoCurrentCaseException ex) { + throw new VolatilityProcessorException(Bundle.VolatilityProcessor_progressMessage_noCurrentCase(), ex); } - - final Case currentCase = Case.getCurrentCase(); + + executableFile = locateVolatilityExecutable(); + if (executableFile == null) { + throw new VolatilityProcessorException(Bundle.VolatilityProcessor_exceptionMessage_volatilityExeNotFound()); + } + fileManager = currentCase.getServices().getFileManager(); - // make a unique folder for this image - moduleOutputPath = currentCase.getModulesOutputDirAbsPath() + File.separator + "Volatility" + File.separator + dataSource.getId(); File directory = new File(String.valueOf(moduleOutputPath)); - if(!directory.exists()){ + /* + * Make an output folder unique to this data source. + */ + Long dataSourceId = dataSource.getId(); + moduleOutputPath = Paths.get(currentCase.getModuleDirectory(), VOLATILITY, dataSourceId.toString()).toString(); + File directory = new File(String.valueOf(moduleOutputPath)); + if (!directory.exists()) { directory.mkdirs(); - progressMonitor.setProgressText("Running imageinfo"); - executeAndParseVolatility("imageinfo"); + progressMonitor.setProgressText(Bundle.VolatilityProcessor_progressMessage_runningImageInfo("imageinfo")); //NON-NLS + runVolatilityPlugin("imageinfo"); //NON-NLS } progressMonitor.setIndeterminate(false); progressMonitor.setProgressMax(pluginsToRun.size()); for (int i = 0; i < pluginsToRun.size(); i++) { - if (isCancelled) + if (isCancelled) { break; + } String pluginToRun = pluginsToRun.get(i); - progressMonitor.setProgressText("Processing " + pluginToRun + " module"); - executeAndParseVolatility(pluginToRun); + runVolatilityPlugin(pluginToRun); progressMonitor.setProgress(i); - } - return false; + } } - + /** - * Get list of error messages that were generated during call to run() - * @return + * Gets a list of error messages that were generated during the processing. + * + * @return The list of error messages. */ List getErrorMessages() { - return errorMsgs; + return new ArrayList<>(errorMsgs); } - private void executeAndParseVolatility(String pluginToRun) { - try { - List commandLine = new ArrayList<>(); - commandLine.add("\"" + executableFile + "\""); - File memoryImage = new File(memoryImagePath); - commandLine.add("--filename=" + memoryImage.getName()); //NON-NLS - - // run imginfo if we haven't run it yet - File imageInfoOutputFile = new File(moduleOutputPath + "\\imageinfo.txt"); - if (imageInfoOutputFile.exists()) { - String memoryProfile = parseImageInfoOutput(imageInfoOutputFile); - if (memoryProfile == null) { - String msg = "Error parsing Volatility imginfo output"; - logger.log(Level.SEVERE, msg); - errorMsgs.add(msg); - return; - } - commandLine.add("--profile=" + memoryProfile); - } - - commandLine.add(pluginToRun); //NON-NLS - - String outputFile = moduleOutputPath + "\\" + pluginToRun + ".txt"; - ProcessBuilder processBuilder = new ProcessBuilder(commandLine); - // Add environment variable to force Volatility to run with the same permissions Autopsy uses - processBuilder.environment().put("__COMPAT_LAYER", "RunAsInvoker"); //NON-NLS - processBuilder.redirectOutput(new File(outputFile)); - processBuilder.redirectError(new File(moduleOutputPath + "\\Volatility_Run.err")); - processBuilder.directory(new File(memoryImage.getParent())); - + /** + * Runs a given Volatility plugin and parses its output to create artifacts. + * + * @param pluginToRun The name of the Volatility plugin to run. + * + * @throws VolatilityProcessorException If there is a critical error, add + * messages to the error messages list + * for non-critical errors. + */ + @NbBundle.Messages({ + "VolatilityProcessor_exceptionMessage_failedToRunVolatilityExe=Could not run Volatility", + "# {0} - plugin name", + "VolatilityProcessor_exceptionMessage_errorRunningPlugin=Volatility error running {0} plugin", + "# {0} - plugin name", + "VolatilityProcessor_exceptionMessage_errorAddingOutput=Failed to add output for {0} to case", + "# {0} - plugin name", + "VolatilityProcessor_exceptionMessage_searchServiceNotFound=Keyword search service not found, output for {0} plugin not indexed", + "# {0} - plugin name", + "VolatilityProcessor_exceptionMessage_errorIndexingOutput=Error indexing output for {0} plugin" + }) + private void runVolatilityPlugin(String pluginToRun) throws VolatilityProcessorException { + progressMonitor.setProgressText("Running module " + pluginToRun); + + List commandLine = new ArrayList<>(); + commandLine.add("\"" + executableFile + "\""); //NON-NLS + File memoryImage = new File(memoryImagePath); + commandLine.add("--filename=" + memoryImage.getName()); //NON-NLS + + File imageInfoOutputFile = new File(moduleOutputPath + "\\imageinfo.txt"); //NON-NLS + if (imageInfoOutputFile.exists()) { + String memoryProfile = parseImageInfoOutput(imageInfoOutputFile); + commandLine.add("--profile=" + memoryProfile); //NON-NLS + } + + commandLine.add(pluginToRun); + + String outputFile = moduleOutputPath + "\\" + pluginToRun + ".txt"; //NON-NLS + ProcessBuilder processBuilder = new ProcessBuilder(commandLine); + /* + * Add an environment variable to force Volatility to run with the same + * permissions Autopsy uses. + */ + processBuilder.environment().put("__COMPAT_LAYER", "RunAsInvoker"); //NON-NLS + processBuilder.redirectOutput(new File(outputFile)); + processBuilder.redirectError(new File(moduleOutputPath + "\\Volatility_Run.err")); //NON-NLS + processBuilder.directory(new File(memoryImage.getParent())); + + try { int exitVal = ExecUtil.execute(processBuilder); if (exitVal != 0) { - String msg = "Volatility non-0 exit value for module: " + pluginToRun; - logger.log(Level.SEVERE, msg); - errorMsgs.add(msg); + errorMsgs.add(Bundle.VolatilityProcessor_exceptionMessage_errorRunningPlugin(pluginToRun)); return; } - - if (isCancelled) - return; - - // add the output to the case - final Case currentCase = Case.getCurrentCase(); - Report report = currentCase.getSleuthkitCase().addReport(outputFile, "Volatility", "Volatility " + pluginToRun + " Module"); - - KeywordSearchService searchService = Lookup.getDefault().lookup(KeywordSearchService.class); - if (null == searchService) { - logger.log(Level.WARNING, "Keyword search service not found. Report will not be indexed"); - } else { - searchService.index(report); - } - - scanOutputFile(pluginToRun, new File(outputFile)); - - } catch (IOException | SecurityException | TskCoreException ex) { - logger.log(Level.SEVERE, "Unable to run Volatility", ex); //NON-NLS - //this.addErrorMessage(NbBundle.getMessage(this.getClass(), "ExtractRegistry.execRegRip.errMsg.failedAnalyzeRegFile", this.getName())); + } catch (IOException | SecurityException ex) { + throw new VolatilityProcessorException(Bundle.VolatilityProcessor_exceptionMessage_failedToRunVolatilityExe(), ex); } - } - - /** - * Finds and returns the path to the executable, if able. - * - * @param executableToFindName The name of the executable to find - * - * @return A File reference or null - */ - private static File locateExecutable() { - // Must be running under a Windows operating system. - if (!PlatformUtil.isWindowsOS()) { - return null; - } - - String executableToFindName = Paths.get(VOLATILITY_DIRECTORY, VOLATILITY_EXECUTABLE).toString(); + if (isCancelled) { + return; + } + + /* + * Add the plugin output file to the case as a report. + */ + try { + Report report = currentCase.getSleuthkitCase().addReport(outputFile, VOLATILITY, VOLATILITY + " " + pluginToRun + " Plugin"); //NON-NLS + try { + KeywordSearchService searchService = Lookup.getDefault().lookup(KeywordSearchService.class); + if (searchService != null) { + searchService.index(report); + } else { + errorMsgs.add(Bundle.VolatilityProcessor_exceptionMessage_searchServiceNotFound(pluginToRun)); + /* + * Log the exception as well as add it to the error + * messages, to ensure that the stack trace is not lost. + */ + logger.log(Level.WARNING, Bundle.VolatilityProcessor_exceptionMessage_errorIndexingOutput(pluginToRun)); + } + } catch (TskCoreException ex) { + throw new VolatilityProcessorException(Bundle.VolatilityProcessor_exceptionMessage_errorIndexingOutput(pluginToRun), ex); + } + } catch (TskCoreException ex) { + throw new VolatilityProcessorException(Bundle.VolatilityProcessor_exceptionMessage_errorAddingOutput(pluginToRun), ex); + } + + createArtifactsFromPluginOutput(pluginToRun, new File(outputFile)); + } + + /** + * Finds and returns the path to the Volatility executable, if able. + * + * @return A File reference or null. + */ + private static File locateVolatilityExecutable() { + if (!PlatformUtil.isWindowsOS()) { + return null; + } + + String executableToFindName = Paths.get(VOLATILITY, VOLATILITY_EXECUTABLE).toString(); File exeFile = InstalledFileLocator.getDefault().locate(executableToFindName, VolatilityProcessor.class.getPackage().getName(), false); if (null == exeFile) { return null; @@ -215,259 +261,281 @@ class VolatilityProcessor { return exeFile; } - private String parseImageInfoOutput(File imageOutputFile) throws FileNotFoundException { - // create a Buffered Reader object instance with a FileReader - try ( - BufferedReader br = new BufferedReader(new FileReader(imageOutputFile))) { - // read the first line from the text file - String fileRead = br.readLine(); - br.close(); - String[] profileLine = fileRead.split(":"); - String[] memProfile = profileLine[1].split(",|\\("); - return memProfile[0].replaceAll("\\s+",""); - } catch (IOException ex) { - Exceptions.printStackTrace(ex); - // @@@ Need to log this or rethrow it - } - - return null; + @NbBundle.Messages({ + "VolatilityProcessor_exceptionMessage_failedToParseImageInfo=Could not parse image info" + }) + private String parseImageInfoOutput(File imageOutputFile) throws VolatilityProcessorException { + try (BufferedReader br = new BufferedReader(new FileReader(imageOutputFile))) { + String fileRead = br.readLine(); + String[] profileLine = fileRead.split(":"); //NON-NLS + String[] memProfile = profileLine[1].split(",|\\("); //NON-NLS + return memProfile[0].replaceAll("\\s+", ""); //NON-NLS + } catch (IOException ex) { + throw new VolatilityProcessorException(Bundle.VolatilityProcessor_exceptionMessage_failedToParseImageInfo(), ex); + } } - - /** - * Lookup the set of files and add INTERESTING_ITEM artifacts for them. - * - * @param fileSet - * @param pluginName - */ - private void flagFiles(Set fileSet, String pluginName) { - - Blackboard blackboard; - try { - blackboard = Case.getCurrentCase().getServices().getBlackboard(); - } - catch (Exception ex) { - // case is closed ?? - return; - } + /** + * Adds interesting file artifacts for files found by a Volatility plugin. + * + * @param fileSet The paths of the files within the memeory image data + * source. + * @param pluginName The name of the source Volatility plugin. + */ + @NbBundle.Messages({ + "# {0} - plugin name", + "VolatilityProcessor_artifactAttribute_interestingFileSet=Volatility Plugin {0}", + "# {0} - file path", + "# {1} - file name", + "# {2} - plugin name", + "VolatilityProcessor_exceptionMessage_fileNotFound=File {0}/{1} not found for ouput of {2} plugin", + "# {0} - plugin name", + "VolatilityProcessor_exceptionMessage_errorCreatingArtifact=Error creating artifact for output of {0} plugin", + "# {0} - plugin name", + "VolatilityProcessor_errorMessage_errorFindingFiles=Error finding files parsed from output of {0} plugin", + "# {0} - plugin name", + "VolatilityProcessor_errorMessage_failedToIndexArtifact=Error indexing artifact from output of {0} plugin" + }) + private void flagFiles(Set fileSet, String pluginName) throws VolatilityProcessorException { + Blackboard blackboard = currentCase.getServices().getBlackboard(); for (String file : fileSet) { if (isCancelled) { - return; + return; } + if (file.isEmpty()) { + continue; + } + File volfile = new File(file); String fileName = volfile.getName().trim(); - // File does not have any data in it based on bad data if (fileName.length() < 1) { continue; } String filePath = volfile.getParent(); - + + logger.log(Level.INFO, "Looking up file " + fileName + " at path " + filePath); + try { List resolvedFiles; if (filePath == null) { - resolvedFiles = fileManager.findFiles(fileName); //NON-NLS + resolvedFiles = fileManager.findFiles(fileName); } else { // File changed the slashes back to \ on us... - filePath = filePath.replaceAll("\\\\", "/"); - resolvedFiles = fileManager.findFiles(fileName, filePath); //NON-NLS + filePath = filePath.replaceAll("\\\\", "/"); //NON-NLS + resolvedFiles = fileManager.findFiles(fileName, filePath); } - + // if we didn't get anything, then try adding a wildcard for extension - if ((resolvedFiles.isEmpty()) && (fileName.contains(".") == false)) { - + if ((resolvedFiles.isEmpty()) && (fileName.contains(".") == false)) { //NON-NLS + // if there is already the same entry with ".exe" in the set, just use that one - if (fileSet.contains(file + ".exe")) + if (fileSet.contains(file + ".exe")) { //NON-NLS continue; - - fileName = fileName + ".%"; + } + + fileName += ".%"; //NON-NLS + logger.log(Level.INFO, "Looking up file (extension wildcard) " + fileName + " at path " + filePath); + if (filePath == null) { resolvedFiles = fileManager.findFiles(fileName); //NON-NLS } else { resolvedFiles = fileManager.findFiles(fileName, filePath); //NON-NLS } - } - + if (resolvedFiles.isEmpty()) { - logger.log(Level.SEVERE, "File not found in lookup: " + filePath + "/" + fileName); - errorMsgs.add("File not found in lookup: " + filePath + "/" + fileName); + errorMsgs.add(Bundle.VolatilityProcessor_exceptionMessage_fileNotFound(filePath, fileName, pluginName)); continue; } - - resolvedFiles.forEach((resolvedFile) -> { + + for (AbstractFile resolvedFile : resolvedFiles) { if (resolvedFile.getType() == TSK_DB_FILES_TYPE_ENUM.SLACK) { - return; // equivalent to continue in non-lambda world + continue; } try { - String MODULE_NAME = "Volatility"; BlackboardArtifact volArtifact = resolvedFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT); - BlackboardAttribute att1 = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME, MODULE_NAME, - "Volatility Plugin " + pluginName); + BlackboardAttribute att1 = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME, VOLATILITY, Bundle.VolatilityProcessor_artifactAttribute_interestingFileSet(pluginName)); volArtifact.addAttribute(att1); try { // index the artifact for keyword search blackboard.indexArtifact(volArtifact); } catch (Blackboard.BlackboardException ex) { - logger.log(Level.SEVERE, "Unable to index blackboard artifact " + volArtifact.getArtifactID(), ex); //NON-NLS + errorMsgs.add(Bundle.VolatilityProcessor_errorMessage_failedToIndexArtifact(pluginName)); + /* + * Log the exception as well as add it to the error + * messages, to ensure that the stack trace is not + * lost. + */ + logger.log(Level.SEVERE, String.format("Failed to index artifact (artifactId=%d) for for output of %s plugin", volArtifact.getArtifactID(), pluginName), ex); } // fire event to notify UI of this new artifact - services.fireModuleDataEvent(new ModuleDataEvent(MODULE_NAME, BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT)); + services.fireModuleDataEvent(new ModuleDataEvent(VOLATILITY, BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT)); } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Failed to create BlackboardArtifact.", ex); // NON-NLS - } catch (IllegalStateException ex) { - logger.log(Level.SEVERE, "Failed to create BlackboardAttribute.", ex); // NON-NLS + throw new VolatilityProcessorException(Bundle.VolatilityProcessor_exceptionMessage_errorCreatingArtifact(pluginName), ex); } - }); + } } catch (TskCoreException ex) { - //String msg = NbBundle.getMessage(this.getClass(), "Chrome.getHistory.errMsg.errGettingFiles"); - logger.log(Level.SEVERE, "Error in Finding Files", ex); - return; + throw new VolatilityProcessorException(Bundle.VolatilityProcessor_errorMessage_errorFindingFiles(pluginName), ex); } } } - - /** - * Scan the output of Volatility and create artifacts as needed - * - * @param pluginName Name of volatility module run - * @param PluginOutput File that contains the output to parse - */ - private void scanOutputFile(String pluginName, File PluginOutput) { - - if (pluginName.matches("dlllist")) { - Set fileSet = parseDllList(PluginOutput); - flagFiles(fileSet, pluginName); - } else if (pluginName.matches("handles")) { - Set fileSet = parseHandles(PluginOutput); - flagFiles(fileSet, pluginName); - } else if (pluginName.matches("cmdline")) { - Set fileSet = parseCmdline(PluginOutput); - flagFiles(fileSet, pluginName); - } else if (pluginName.matches("psxview")){ - Set fileSet = parsePsxview(PluginOutput); - flagFiles(fileSet, pluginName); - } else if (pluginName.matches("pslist")) { - Set fileSet = parsePslist(PluginOutput); - flagFiles(fileSet, pluginName); - } else if (pluginName.matches("psscan")) { - Set fileSet = parsePsscan(PluginOutput); - flagFiles(fileSet, pluginName); - } else if (pluginName.matches("pstree")) { - Set fileSet = parsePstree(PluginOutput); - flagFiles(fileSet, pluginName); - } else if (pluginName.matches("svcscan")) { - Set fileSet = parseSvcscan(PluginOutput); - flagFiles(fileSet, pluginName); - } else if (pluginName.matches("filescan")) { - // BC: Commented out. Too many hits to flag - //Set fileSet = ParseFilescan(PluginOutput); - //lookupFiles(fileSet, pluginName); - } else if (pluginName.matches("shimcache")) { - Set fileSet = parseShimcache(PluginOutput); - flagFiles(fileSet, pluginName); - } - } - /** - * Normalize the path we parse out of the output before - * we look it up in the case DB - * - * @param filePath Path to normalize - * @return Normalized version + /** + * Parses the output of a Volatility plugin and creates artifacts as needed. + * + * @param pluginName Name of the Volatility plugin. + * @param pluginOutputFile File that contains the output to parse. + */ + private void createArtifactsFromPluginOutput(String pluginName, File pluginOutputFile) throws VolatilityProcessorException { + progressMonitor.setProgressText("Parsing module " + pluginName); + Set fileSet = null; + switch (pluginName) { + case "dlllist": //NON-NLS + fileSet = parseDllListOutput(pluginOutputFile); + break; + case "handles": //NON-NLS + fileSet = parseHandlesOutput(pluginOutputFile); + break; + case "cmdline": //NON-NLS + fileSet = parseCmdlineOutput(pluginOutputFile); + break; + case "psxview": //NON-NLS + fileSet = parsePsxviewOutput(pluginOutputFile); + break; + case "pslist": //NON-NLS + fileSet = parsePslistOutput(pluginOutputFile); + break; + case "psscan": //NON-NLS + fileSet = parsePsscanOutput(pluginOutputFile); + break; + case "pstree": //NON-NLS + fileSet = parsePstreeOutput(pluginOutputFile); + break; + case "svcscan": //NON-NLS + fileSet = parseSvcscanOutput(pluginOutputFile); + break; + case "shimcache": //NON-NLS + fileSet = parseShimcacheOutput(pluginOutputFile); + break; + default: + break; + } + + if (fileSet != null && !fileSet.isEmpty()) { + progressMonitor.setProgressText("Flagging files from module " + pluginName); + flagFiles(fileSet, pluginName); + } + } + + /** + * Normalizes a file path from a Volatility plugin so it can be used to look + * up the file in the case database. + * + * @param filePath Path to normalize. + * + * @return The normalized path or the empty string if the path cannot be + * normalized or should be ignored. */ private String normalizePath(String filePath) { - if (filePath == null) - return ""; - - filePath = filePath.trim(); + if (filePath == null) { + return ""; //NON-NLS + } + String path = filePath.trim(); + // change slash direction - filePath = filePath.replaceAll("\\\\", "/"); - filePath = filePath.toLowerCase(); + path = path.replaceAll("\\\\", "/"); //NON-NLS + path = path.toLowerCase(); // \??\c:\windows ... - if ((filePath.length() > 4) && (filePath.startsWith("/??/"))) { - filePath = filePath.substring(4); + if ((path.length() > 4) && (path.startsWith("/??/"))) { //NON-NLS + path = path.substring(4); } // strip C: - if (filePath.contains(":")) { - int index = filePath.indexOf(':'); - if (index+1 < filePath.length()) { - filePath = filePath.substring(index + 1); - } + if (path.contains(":")) { //NON-NLS + int index = path.indexOf(":"); + if (index+1 < path.length()) + path = path.substring(index + 1); } - - - filePath = filePath.replaceAll("/systemroot/", "/windows/"); + + path = path.replaceAll("/systemroot/", "/windows/"); + // catches 1 type of file in cmdline - filePath = filePath.replaceAll("%systemroot%", "/windows/"); - filePath = filePath.replaceAll("/device/",""); + path = path.replaceAll("%systemroot%", "/windows/"); //NON-NLS + path = path.replaceAll("/device/", ""); //NON-NLS // helps with finding files in handles plugin // example: \Device\clfs\Device\HarddiskVolume2\Users\joe\AppData\Local\Microsoft\Windows\UsrClass.dat{e15d4b01-1598-11e8-93e6-080027b5e733}.TM - if (filePath.contains("/harddiskvolume")) { + if (path.contains("/harddiskvolume")) { //NON-NLS // 16 advances beyond harddiskvolume and the number - int index = filePath.indexOf("/harddiskvolume"); - if (index+16 < filePath.length()) { - filePath = filePath.substring(index + 16); + int index = path.indexOf("/harddiskvolume"); //NON-NLS + if (index+16 < path.length()) { + path = path.substring(index + 16); } } - - // no point returning these. We won't map to them - if (filePath.startsWith("/namedpipe/")) - return ""; - return filePath; + // no point returning these. We won't map to them + if (path.startsWith("/namedpipe/")) { //NON-NLS + return ""; //NON-NLS + } + + return path; } - - private Set parseHandles(File pluginFile) { + + @NbBundle.Messages({ + "# {0} - plugin name", + "VolatilityProcessor_errorMessage_outputParsingError=Error parsing output for {0} plugin" + }) + private Set parseHandlesOutput(File pluginOutputFile) { String line; Set fileSet = new HashSet<>(); - try { - BufferedReader br = new BufferedReader(new FileReader(pluginFile)); - // Ignore the first two header lines - br.readLine(); - br.readLine(); - while ((line = br.readLine()) != null) { - // 0x89ab7878 4 0x718 0x2000003 File \Device\HarddiskVolume1\Documents and Settings\QA\Local Settings\Application - if (line.startsWith("0x") == false) - continue; - - String TAG = " File "; - String file_path = null; - if ((line.contains(TAG)) && (line.length() > 57)) { + try (BufferedReader br = new BufferedReader(new FileReader(pluginOutputFile))) { + // Ignore the first two header lines + br.readLine(); + br.readLine(); + while ((line = br.readLine()) != null) { + // 0x89ab7878 4 0x718 0x2000003 File \Device\HarddiskVolume1\Documents and Settings\QA\Local Settings\Application + if (line.startsWith("0x") == false) { //NON-NLS + continue; + } + + String TAG = " File "; //NON-NLS + String file_path; + if ((line.contains(TAG)) && (line.length() > 57)) { file_path = line.substring(57); - if (file_path.contains("\"")) { - file_path = file_path.substring(0, file_path.indexOf('\"')); + if (file_path.contains("\"")) { //NON-NLS + file_path = file_path.substring(0, file_path.indexOf('\"')); //NON-NLS } // this file has a lot of device entries that are not files - if (file_path.startsWith("\\Device\\")) { - if (file_path.contains("HardDiskVolume") == false) + if (file_path.startsWith("\\Device\\")) { //NON-NLS + if (file_path.contains("HardDiskVolume") == false) { //NON-NLS continue; + } } - + fileSet.add(normalizePath(file_path)); - } - } - br.close(); + } + } } catch (IOException ex) { - String msg = "Error parsing handles output"; - logger.log(Level.SEVERE, msg, ex); - errorMsgs.add(msg); - } + errorMsgs.add(Bundle.VolatilityProcessor_errorMessage_outputParsingError("handles")); + /* + * Log the exception as well as add it to the error messages, to + * ensure that the stack trace is not lost. + */ + logger.log(Level.SEVERE, Bundle.VolatilityProcessor_errorMessage_outputParsingError("handles"), ex); + } return fileSet; } - - private Set parseDllList(File pluginFile) { + + private Set parseDllListOutput(File outputFile) { Set fileSet = new HashSet<>(); // read the first line from the text file - try (BufferedReader br = new BufferedReader(new FileReader(pluginFile))) { + try (BufferedReader br = new BufferedReader(new FileReader(outputFile))) { String line; - while ((line = br.readLine()) != null) { - + while ((line = br.readLine()) != null) { // we skip the Command Line entries because that data // is also in the 0x lines (and is more likely to have a full path there. @@ -480,63 +548,39 @@ class VolatilityProcessor { } } } catch (IOException ex) { - String msg = "Error parsing dlllist output"; - logger.log(Level.SEVERE, msg, ex); - errorMsgs.add(msg); - } - return fileSet; + errorMsgs.add(Bundle.VolatilityProcessor_errorMessage_outputParsingError("dlllist")); + /* + * Log the exception as well as add it to the error messages, to + * ensure that the stack trace is not lost. + */ + logger.log(Level.SEVERE, Bundle.VolatilityProcessor_errorMessage_outputParsingError("dlllist"), ex); + } + return fileSet; } - - private Set parseFilescan(File PluginFile) { - String line; - Set fileSet = new HashSet<>(); - try { - BufferedReader br = new BufferedReader(new FileReader(PluginFile)); - // read the first line from the text file - while ((line = br.readLine()) != null) { - try { - if (line.length() < 41) { - continue; - } - String file_path = line.substring(41); - fileSet.add(normalizePath(file_path)); - } catch (StringIndexOutOfBoundsException ex) { - // TO DO Catch exception - } - } - br.close(); - } catch (IOException ex) { - String msg = "Error parsing filescan output"; - logger.log(Level.SEVERE, msg, ex); - errorMsgs.add(msg); - } - return fileSet; - } - - private Set parseCmdline(File PluginFile) { + + private Set parseCmdlineOutput(File outputFile) { Set fileSet = new HashSet<>(); // read the first line from the text file - try (BufferedReader br = new BufferedReader(new FileReader(PluginFile))) { + try (BufferedReader br = new BufferedReader(new FileReader(outputFile))) { String line; while ((line = br.readLine()) != null) { if (line.length() > 16) { - String TAG = "Command line : "; + String TAG = "Command line : "; //NON-NLS if ((line.startsWith(TAG)) && line.length() > TAG.length() + 1) { String file_path; // Command line : "C:\Program Files\VMware\VMware Tools\vmacthlp.exe" // grab whats inbetween the quotes - if (line.charAt(TAG.length()) == '\"') { + if (line.charAt(TAG.length()) == '\"') { //NON-NLS file_path = line.substring(TAG.length() + 1); - if (file_path.contains("\"")) { - file_path = file_path.substring(0, file_path.indexOf('\"')); - } - } - // Command line : C:\WINDOWS\system32\csrss.exe ObjectDirectory=\Windows SharedSection=1024,3072,512 + if (file_path.contains("\"")) { //NON-NLS + file_path = file_path.substring(0, file_path.indexOf('\"')); //NON-NLS + } + } // Command line : C:\WINDOWS\system32\csrss.exe ObjectDirectory=\Windows SharedSection=1024,3072,512 // grab everything before the next space - we don't want arguments else { file_path = line.substring(TAG.length()); - if (file_path.contains(" ")) { + if (file_path.contains(" ")) { //NON-NLS file_path = file_path.substring(0, file_path.indexOf(' ')); } } @@ -544,176 +588,189 @@ class VolatilityProcessor { } } } - + } catch (IOException ex) { - String msg = "Error parsing cmdline output"; - logger.log(Level.SEVERE, msg, ex); - errorMsgs.add(msg); - } - return fileSet; + errorMsgs.add(Bundle.VolatilityProcessor_errorMessage_outputParsingError("cmdline")); + /* + * Log the exception as well as add it to the error messages, to + * ensure that the stack trace is not lost. + */ + logger.log(Level.SEVERE, Bundle.VolatilityProcessor_errorMessage_outputParsingError("cmdline"), ex); + } + return fileSet; } - - private Set parseShimcache(File PluginFile) { + + private Set parseShimcacheOutput(File outputFile) { String line; Set fileSet = new HashSet<>(); - try { - BufferedReader br = new BufferedReader(new FileReader(PluginFile)); - // ignore the first 2 header lines - br.readLine(); - br.readLine(); - while ((line = br.readLine()) != null) { + try (BufferedReader br = new BufferedReader(new FileReader(outputFile))) { + // ignore the first 2 header lines + br.readLine(); + br.readLine(); + while ((line = br.readLine()) != null) { String file_path; //1970-01-01 00:00:00 UTC+0000 2017-10-25 13:07:30 UTC+0000 C:\WINDOWS\system32\msctfime.ime //2017-10-23 20:47:40 UTC+0000 2017-10-23 20:48:02 UTC+0000 \??\C:\WINDOWS\CT_dba9e71b-ad55-4132-a11b-faa946b197d6.exe if (line.length() > 62) { file_path = line.substring(62); - if (file_path.contains("\"")) { - file_path = file_path.substring(0, file_path.indexOf('\"')); - } + if (file_path.contains("\"")) { //NON-NLS + file_path = file_path.substring(0, file_path.indexOf('\"')); //NON-NLS + } fileSet.add(normalizePath(file_path)); - } - } - br.close(); - } catch (IOException ex) { - String msg = "Error parsing shimcache output"; - logger.log(Level.SEVERE, msg, ex); - errorMsgs.add(msg); - } - return fileSet; + } + } + } catch (IOException ex) { + errorMsgs.add(Bundle.VolatilityProcessor_errorMessage_outputParsingError("shimcache")); + /* + * Log the exception as well as add it to the error messages, to + * ensure that the stack trace is not lost. + */ + logger.log(Level.SEVERE, Bundle.VolatilityProcessor_errorMessage_outputParsingError("shimcache"), ex); + } + return fileSet; } - - private Set parsePsscan(File PluginFile) { + + private Set parsePsscanOutput(File outputFile) { String line; Set fileSet = new HashSet<>(); - try { - BufferedReader br = new BufferedReader(new FileReader(PluginFile)); - // ignore the first two header lines - br.readLine(); - br.readLine(); - while ((line = br.readLine()) != null) { + try (BufferedReader br = new BufferedReader(new FileReader(outputFile))) { + // ignore the first two header lines + br.readLine(); + br.readLine(); + while ((line = br.readLine()) != null) { // 0x000000000969a020 notepad.exe 3604 3300 0x16d40340 2018-01-12 14:41:16 UTC+0000 - if (line.startsWith("0x") == false) - continue; - if (line.length() < 37) { + if (line.startsWith("0x") == false) { //NON-NLS continue; } + else if (line.length() < 37) { + continue; + } + String file_path = line.substring(19, 37); file_path = normalizePath(file_path); - + // ignore system, it's not really a path - if (file_path.equals("system")) + if (file_path.equals("system")) { //NON-NLS continue; + } fileSet.add(file_path); - } - br.close(); - } catch (IOException ex) { - String msg = "Error parsing psscan output"; - logger.log(Level.SEVERE, msg, ex); - errorMsgs.add(msg); - } - return fileSet; + } + } catch (IOException ex) { + errorMsgs.add(Bundle.VolatilityProcessor_errorMessage_outputParsingError("psscan")); + /* + * Log the exception as well as add it to the error messages, to + * ensure that the stack trace is not lost. + */ + logger.log(Level.SEVERE, Bundle.VolatilityProcessor_errorMessage_outputParsingError("psscan"), ex); + } + return fileSet; } - private Set parsePslist(File PluginFile) { + private Set parsePslistOutput(File outputFile) { String line; Set fileSet = new HashSet<>(); - try { - BufferedReader br = new BufferedReader(new FileReader(PluginFile)); - // read the first line from the text file - while ((line = br.readLine()) != null) { - if (line.startsWith("0x") == false) { - continue; - } - + try (BufferedReader br = new BufferedReader(new FileReader(outputFile))) { + // read the first line from the text file + while ((line = br.readLine()) != null) { + if (line.startsWith("0x") == false) { //NON-NLS + continue; + } + // 0x89cfb998 csrss.exe 704 640 14 532 0 0 2017-12-07 14:05:34 UTC+0000 - else if (line.length() < 34) { - continue; - } - - String file_path = line.substring(10, 34); - file_path = normalizePath(file_path); - - // ignore system, it's not really a path - if (file_path.equals("system")) { - continue; - } - fileSet.add(file_path); - } - br.close(); - } catch (IOException ex) { - String msg = "Error parsing pslist output"; - logger.log(Level.SEVERE, msg, ex); - errorMsgs.add(msg); - } - return fileSet; - } - - private Set parsePsxview(File PluginFile) { - String line; - Set fileSet = new HashSet<>(); - try { - BufferedReader br = new BufferedReader(new FileReader(PluginFile)); - // ignore the first two header lines - br.readLine(); - br.readLine(); - while ((line = br.readLine()) != null) { - // 0x09adf980 svchost.exe 1368 True True False True True True True - if (line.startsWith("0x") == false) - continue; if (line.length() < 34) continue; - String file_path = line.substring(11, 34); + String file_path = line.substring(10, 34); file_path = normalizePath(file_path); - + // ignore system, it's not really a path - if (file_path.equals("system")) { + if (file_path.equals("system")) { //NON-NLS continue; } fileSet.add(file_path); - } - br.close(); - } catch (IOException ex) { - String msg = "Error parsing psxview output"; - logger.log(Level.SEVERE, msg, ex); - errorMsgs.add(msg); - } - return fileSet; + } + } catch (IOException ex) { + errorMsgs.add(Bundle.VolatilityProcessor_errorMessage_outputParsingError("pslist")); + /* + * Log the exception as well as add it to the error messages, to + * ensure that the stack trace is not lost. + */ + logger.log(Level.SEVERE, Bundle.VolatilityProcessor_errorMessage_outputParsingError("pslist"), ex); + } + return fileSet; } - private Set parsePstree(File PluginFile) { + private Set parsePsxviewOutput(File outputFile) { String line; Set fileSet = new HashSet<>(); - try { - BufferedReader br = new BufferedReader(new FileReader(PluginFile)); - // read the first line from the text file - while ((line = br.readLine()) != null) { - // ... 0x897e5020:services.exe 772 728 15 287 2017-12-07 14:05:35 UTC+000 + try (BufferedReader br = new BufferedReader(new FileReader(outputFile))) { + // ignore the first two header lines + br.readLine(); + br.readLine(); + while ((line = br.readLine()) != null) { + // 0x09adf980 svchost.exe 1368 True True False True True True True + if (line.startsWith("0x") == false) { //NON-NLS + continue; + } + + if (line.length() < 34) { + continue; + } + + String file_path = line.substring(11, 34); + file_path = normalizePath(file_path); + + // ignore system, it's not really a path + if (file_path.equals("system")) { //NON-NLS + continue; + } + fileSet.add(file_path); + } + } catch (IOException ex) { + errorMsgs.add(Bundle.VolatilityProcessor_errorMessage_outputParsingError("psxview")); + /* + * Log the exception as well as add it to the error messages, to + * ensure that the stack trace is not lost. + */ + logger.log(Level.SEVERE, Bundle.VolatilityProcessor_errorMessage_outputParsingError("psxview"), ex); + } + return fileSet; + } + + private Set parsePstreeOutput(File outputFile) { + String line; + Set fileSet = new HashSet<>(); + try (BufferedReader br = new BufferedReader(new FileReader(outputFile))) { + // read the first line from the text file + while ((line = br.readLine()) != null) { + // ... 0x897e5020:services.exe 772 728 15 287 2017-12-07 14:05:35 UTC+000 String TAG = ":"; if (line.contains(TAG)) { int index = line.indexOf(TAG); if (line.length() < 52 || index + 1 >= 52) { continue; } - String file_path = line.substring(line.indexOf(TAG) + 1, 52); + String file_path = line.substring(line.indexOf(':') + 1, 52); //NON-NLS file_path = normalizePath(file_path); - + // ignore system, it's not really a path - if (file_path.equals("system")) { + if (file_path.equals("system")) { //NON-NLS continue; } fileSet.add(file_path); } - } - br.close(); - } catch (IOException ex) { - String msg = "Error parsing pstree output"; - logger.log(Level.SEVERE, msg, ex); - errorMsgs.add(msg); - } - return fileSet; + } + } catch (IOException ex) { + errorMsgs.add(Bundle.VolatilityProcessor_errorMessage_outputParsingError("pstree")); + /* + * Log the exception as well as add it to the error messages, to + * ensure that the stack trace is not lost. + */ + logger.log(Level.SEVERE, Bundle.VolatilityProcessor_errorMessage_outputParsingError("pstree"), ex); + } + return fileSet; } - private Set parseSvcscan(File PluginFile) { + private Set parseSvcscanOutput(File PluginFile) { String line; Set fileSet = new HashSet<>(); try { @@ -758,8 +815,28 @@ class VolatilityProcessor { } return fileSet; } - + + /** + * Requests cancellation of processing. + */ void cancel() { isCancelled = true; } + + /** + * Exception type thrown when the processor experiences an error condition. + */ + final class VolatilityProcessorException extends Exception { + + private static final long serialVersionUID = 1L; + + private VolatilityProcessorException(String message) { + super(message); + } + + private VolatilityProcessorException(String message, Throwable cause) { + super(message, cause); + } + } + } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java index f956513eca..b9c4541c7b 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java @@ -27,7 +27,7 @@ import org.apache.solr.common.SolrInputDocument; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.ContentUtils; -import org.sleuthkit.autopsy.healthmonitor.ServicesHealthMonitor; +import org.sleuthkit.autopsy.healthmonitor.EnterpriseHealthMonitor; import org.sleuthkit.autopsy.healthmonitor.TimingMetric; import org.sleuthkit.autopsy.ingest.IngestJobContext; import org.sleuthkit.autopsy.keywordsearch.Chunker.Chunk; @@ -237,9 +237,9 @@ class Ingester { try { //TODO: consider timeout thread, or vary socket timeout based on size of indexed content - TimingMetric metric = ServicesHealthMonitor.getTimingMetric("Solr: Index chunk"); + TimingMetric metric = EnterpriseHealthMonitor.getTimingMetric("Solr: Index chunk"); solrServer.addDocument(updateDoc); - ServicesHealthMonitor.submitTimingMetric(metric); + EnterpriseHealthMonitor.submitTimingMetric(metric); uncommitedIngests = true; } catch (KeywordSearchModuleException | NoOpenCoreException ex) { diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/RegexQuery.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/RegexQuery.java index ad0fc0a678..1ddd06680e 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/RegexQuery.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/RegexQuery.java @@ -645,18 +645,18 @@ final class RegexQuery implements KeywordSearchQuery { */ static private void addAttributeIfNotAlreadyCaptured(Map attributeMap, ATTRIBUTE_TYPE attrType, String groupName, Matcher matcher) { BlackboardAttribute.Type type = new BlackboardAttribute.Type(attrType); - attributeMap.computeIfAbsent(type, t -> { + + if( ! attributeMap.containsKey(type)) { String value = matcher.group(groupName); if (attrType.equals(ATTRIBUTE_TYPE.TSK_CARD_NUMBER)) { attributeMap.put(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_KEYWORD), new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_KEYWORD, MODULE_NAME, value)); value = CharMatcher.anyOf(" -").removeFrom(value); } + if (StringUtils.isNotBlank(value)) { - return new BlackboardAttribute(attrType, MODULE_NAME, value); - } else { - return null; + attributeMap.put(type, new BlackboardAttribute(attrType, MODULE_NAME, value)); } - }); + } } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java index 5748524ada..eea4d2914e 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java @@ -70,7 +70,7 @@ import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ModuleSettings; import org.sleuthkit.autopsy.coreutils.PlatformUtil; -import org.sleuthkit.autopsy.healthmonitor.ServicesHealthMonitor; +import org.sleuthkit.autopsy.healthmonitor.EnterpriseHealthMonitor; import org.sleuthkit.autopsy.healthmonitor.TimingMetric; import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchServiceException; import org.sleuthkit.datamodel.Content; @@ -775,9 +775,9 @@ public class Server { IndexingServerProperties properties = getMultiUserServerProperties(theCase.getCaseDirectory()); currentSolrServer = new HttpSolrServer("http://" + properties.getHost() + ":" + properties.getPort() + "/solr"); //NON-NLS } - TimingMetric metric = ServicesHealthMonitor.getTimingMetric("Solr: Connectivity check"); + TimingMetric metric = EnterpriseHealthMonitor.getTimingMetric("Solr: Connectivity check"); connectToSolrServer(currentSolrServer); - ServicesHealthMonitor.submitTimingMetric(metric); + EnterpriseHealthMonitor.submitTimingMetric(metric); } catch (SolrServerException | IOException ex) { throw new KeywordSearchModuleException(NbBundle.getMessage(Server.class, "Server.connect.exception.msg", ex.getLocalizedMessage()), ex);