diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardAddingProgressPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardAddingProgressPanel.java index b9ea8f1ae0..6106914e5b 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardAddingProgressPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardAddingProgressPanel.java @@ -48,6 +48,7 @@ import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.runIngestModuleWizard.ShortcutWizardDescriptorPanel; import org.sleuthkit.datamodel.Content; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.Host; /** * The final panel of the add image wizard. It displays a progress bar and @@ -115,7 +116,7 @@ class AddImageWizardAddingProgressPanel extends ShortcutWizardDescriptorPanel { } }); } - + @Override public void setProgressMax(final int max) { // update the progress bar asynchronously @@ -289,7 +290,7 @@ class AddImageWizardAddingProgressPanel extends ShortcutWizardDescriptorPanel { * * * @param errorString the error string to be displayed - * @param critical true if this is a critical error + * @param critical true if this is a critical error */ void addErrors(String errorString, boolean critical) { getComponent().showErrors(errorString, critical); @@ -302,7 +303,7 @@ class AddImageWizardAddingProgressPanel extends ShortcutWizardDescriptorPanel { private void startIngest() { if (!newContents.isEmpty() && readyToIngest && !ingested) { ingested = true; - if (dsProcessor != null && ! dsProcessor.supportsIngestStream()) { + if (dsProcessor != null && !dsProcessor.supportsIngestStream()) { IngestManager.getInstance().queueIngestJob(newContents, ingestJobSettings); } setStateFinished(); @@ -323,8 +324,13 @@ class AddImageWizardAddingProgressPanel extends ShortcutWizardDescriptorPanel { /** * Starts the Data source processing by kicking off the selected * DataSourceProcessor + * + * @param dsp The data source processor providing configuration for how to + * process the specific data source type. + * @param selectedHost The host to which this data source belongs or null + * for a default host. */ - void startDataSourceProcessing(DataSourceProcessor dsp) { + void startDataSourceProcessing(DataSourceProcessor dsp, Host selectedHost) { if (dsProcessor == null) { //this can only be run once final UUID dataSourceId = UUID.randomUUID(); newContents.clear(); @@ -349,7 +355,7 @@ class AddImageWizardAddingProgressPanel extends ShortcutWizardDescriptorPanel { try { Case.getCurrentCaseThrows().notifyAddingDataSource(dataSourceId); } catch (NoCurrentCaseException ex) { - Logger.getLogger(AddImageWizardAddingProgressVisual.class.getName()).log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS + Logger.getLogger(AddImageWizardAddingProgressVisual.class.getName()).log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS } }).start(); DataSourceProcessorCallback cbObj = new DataSourceProcessorCallback() { @@ -365,9 +371,9 @@ class AddImageWizardAddingProgressPanel extends ShortcutWizardDescriptorPanel { if (dsProcessor.supportsIngestStream()) { // Set readyToIngest to false to prevent the wizard from starting ingest a second time. readyToIngest = false; - dsProcessor.runWithIngestStream(ingestJobSettings, getDSPProgressMonitorImpl(), cbObj); + dsProcessor.runWithIngestStream(selectedHost, ingestJobSettings, getDSPProgressMonitorImpl(), cbObj); } else { - dsProcessor.run(getDSPProgressMonitorImpl(), cbObj); + dsProcessor.run(selectedHost, getDSPProgressMonitorImpl(), cbObj); } } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardIngestConfigPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardIngestConfigPanel.java index cfcbdd015c..065a847760 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardIngestConfigPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardIngestConfigPanel.java @@ -43,7 +43,7 @@ import org.sleuthkit.autopsy.ingest.runIngestModuleWizard.ShortcutWizardDescript @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives class AddImageWizardIngestConfigPanel extends ShortcutWizardDescriptorPanel { - @Messages("AddImageWizardIngestConfigPanel.name.text=Configure Ingest Modules") + @Messages("AddImageWizardIngestConfigPanel.name.text=Configure Ingest") private final IngestJobSettingsPanel ingestJobSettingsPanel; /** * The visual component that displays this panel. If you need to access the diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardIterator.java b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardIterator.java index 045bf775b2..9ed66765d3 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardIterator.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardIterator.java @@ -29,6 +29,7 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.ingest.IngestProfiles; import org.sleuthkit.autopsy.ingest.runIngestModuleWizard.IngestProfileSelectionWizardPanel; import org.sleuthkit.autopsy.ingest.runIngestModuleWizard.ShortcutWizardDescriptorPanel; +import org.sleuthkit.datamodel.Host; /** * The iterator class for the "Add Image" wizard panel. This class is used to @@ -41,8 +42,10 @@ class AddImageWizardIterator implements WizardDescriptor.Iterator getPanels() { if (panels == null) { panels = new ArrayList<>(); + hostPanel = new AddImageWizardSelectHostPanel(); + panels.add(hostPanel); + hostPanelIndex = panels.indexOf(hostPanel); AddImageWizardSelectDspPanel dspSelection = new AddImageWizardSelectDspPanel(); panels.add(dspSelection); AddImageWizardAddingProgressPanel progressPanel = new AddImageWizardAddingProgressPanel(action); - AddImageWizardDataSourceSettingsPanel dsPanel = new AddImageWizardDataSourceSettingsPanel(); AddImageWizardIngestConfigPanel ingestConfigPanel = new AddImageWizardIngestConfigPanel(progressPanel); panels.add(dsPanel); @@ -164,7 +169,7 @@ class AddImageWizardIterator implements WizardDescriptor.Iterator 0); //Users should be able to back up to select a different DSP } /** @@ -180,8 +185,10 @@ class AddImageWizardIterator implements WizardDescriptor.Iterator sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.casemodule; + +import java.awt.Component; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import javax.swing.event.ChangeListener; +import org.openide.WizardDescriptor; +import org.openide.util.ChangeSupport; +import org.openide.util.HelpCtx; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.datamodel.Host; +import org.sleuthkit.autopsy.ingest.runIngestModuleWizard.ShortcutWizardDescriptorPanel; + +/** + * Create a wizard panel which contains a panel allowing the selection of a host + * for a data source. + */ +@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives +@Messages("AddImageWizardSelectHostPanel_title=Select Host To Add The Data Source To") +final class AddImageWizardSelectHostPanel extends ShortcutWizardDescriptorPanel implements PropertyChangeListener { + + private final AddImageWizardSelectHostVisual component = new AddImageWizardSelectHostVisual(); + private final ChangeSupport changeSupport = new ChangeSupport(this); + + AddImageWizardSelectHostPanel() { + component.addListener(this); + } + + @Override + public Component getComponent() { + return component; + } + + @Override + public HelpCtx getHelp() { + return HelpCtx.DEFAULT_HELP; + } + + @Override + public void readSettings(WizardDescriptor data) { + } + + /** + * Returns or generates the selected host. If user specifies 'generate + * new...', then null will be returned. + * + * @return The selected host or null if to be auto generated. + */ + Host getSelectedHost() { + return component.getSelectedHost(); + } + + @Override + public void storeSettings(WizardDescriptor data) { + } + + @Override + public boolean isValid() { + return component.hasValidData(); + } + + @Override + public void addChangeListener(ChangeListener cl) { + changeSupport.addChangeListener(cl); + } + + @Override + public void removeChangeListener(ChangeListener cl) { + changeSupport.removeChangeListener(cl); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + changeSupport.fireChange(); + } + + +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardSelectHostVisual.form b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardSelectHostVisual.form new file mode 100644 index 0000000000..bdb977932a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardSelectHostVisual.form @@ -0,0 +1,163 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardSelectHostVisual.java b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardSelectHostVisual.java new file mode 100644 index 0000000000..9197b2fb42 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardSelectHostVisual.java @@ -0,0 +1,374 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.casemodule; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.util.Collection; +import java.util.Objects; +import java.util.Set; +import java.util.Vector; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.apache.commons.lang.StringUtils; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datamodel.hosts.HostNameValidator; +import org.sleuthkit.datamodel.Host; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Panel to be displayed as a part of the add datasource wizard. Provides the + * ability to select current host. + */ +@Messages({ + "AddImageWizardSelectHostVisual_title=Select Host" +}) +class AddImageWizardSelectHostVisual extends javax.swing.JPanel { + + /** + * A combo box item for a host (or null for default). + */ + private static class HostListItem { + + private final Host host; + + /** + * Main constructor. + * + * @param host The host. + */ + HostListItem(Host host) { + this.host = host; + } + + /** + * @return The host. + */ + Host getHost() { + return host; + } + + @Override + public String toString() { + if (host == null || host.getName() == null) { + return ""; + } else { + return host.getName(); + } + } + + @Override + public int hashCode() { + int hash = 7; + hash = 41 * hash + Objects.hashCode(this.host == null ? 0 : this.host.getId()); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final HostListItem other = (HostListItem) obj; + if (!Objects.equals( + this.host == null ? 0 : this.host.getId(), + other.host == null ? 0 : other.host.getId())) { + + return false; + } + return true; + } + + } + + private static final Logger logger = Logger.getLogger(AddImageWizardSelectHostVisual.class.getName()); + + private final PropertyChangeSupport changeSupport = new PropertyChangeSupport(this); + private Set sanitizedHostSet = null; + + /** + * Creates new form SelectHostPanel + */ + AddImageWizardSelectHostVisual() { + initComponents(); + + specifyNewHostTextField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void changedUpdate(DocumentEvent e) { + refresh(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + refresh(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + refresh(); + } + }); + + existingHostList.addListSelectionListener((evt) -> refresh()); + + loadHostData(); + refresh(); + } + + /** + * Add listener for validation change events. + * + * @param pcl The property change listener. + */ + void addListener(PropertyChangeListener pcl) { + changeSupport.addPropertyChangeListener(pcl); + } + + /** + * Remove listener from validation change events. + * + * @param pcl The property change listener. + */ + void removeListener(PropertyChangeListener pcl) { + changeSupport.removePropertyChangeListener(pcl); + } + + /** + * @return The currently selected host or null if no selection. This will + * generate a new host if 'Specify New Host Name' + */ + Host getSelectedHost() { + if (specifyNewHostRadio.isSelected() && StringUtils.isNotEmpty(specifyNewHostTextField.getText())) { + String newHostName = specifyNewHostTextField.getText(); + try { + return Case.getCurrentCaseThrows().getSleuthkitCase().getHostManager().createHost(newHostName); + } catch (NoCurrentCaseException | TskCoreException ex) { + logger.log(Level.WARNING, String.format("Unable to create host '%s'.", newHostName), ex); + return null; + } + } else if (useExistingHostRadio.isSelected() + && existingHostList.getSelectedValue() != null + && existingHostList.getSelectedValue().getHost() != null) { + + return existingHostList.getSelectedValue().getHost(); + } else { + return null; + } + } + + /** + * Loads hosts from database and displays in combo box. + */ + private void loadHostData() { + try { + Collection hosts = Case.getCurrentCaseThrows().getSleuthkitCase().getHostManager().getHosts(); + sanitizedHostSet = HostNameValidator.getSanitizedHostNames(hosts); + + Vector hostListItems = hosts.stream() + .filter(h -> h != null) + .sorted((a, b) -> getNameOrEmpty(a).compareToIgnoreCase(getNameOrEmpty(b))) + .map((h) -> new HostListItem(h)) + .collect(Collectors.toCollection(Vector::new)); + + existingHostList.setListData(hostListItems); + } catch (NoCurrentCaseException | TskCoreException ex) { + logger.log(Level.WARNING, "Unable to display host items with no current case.", ex); + } + } + + /** + * Returns the name of the host or an empty string if the host or host name + * is null. + * + * @param host The host. + * @return The host name or empty string. + */ + private String getNameOrEmpty(Host host) { + return host == null || host.getName() == null ? "" : host.getName(); + } + + private void refresh() { + specifyNewHostTextField.setEnabled(specifyNewHostRadio.isSelected()); + existingHostList.setEnabled(useExistingHostRadio.isSelected()); + + String prevValidationMessage = validationMessage.getText(); + String newValidationMessage = getValidationMessage(); + validationMessage.setText(newValidationMessage); + // if validation message changed (empty to non-empty or vice-versa) fire validation update + if (StringUtils.isBlank(prevValidationMessage) != StringUtils.isBlank(newValidationMessage)) { + changeSupport.firePropertyChange("validation", prevValidationMessage, newValidationMessage); + } + } + + @Messages({ + "AddImageWizardSelectHostVisual_getValidationMessage_noHostSelected=Please select an existing host.",}) + private String getValidationMessage() { + if (specifyNewHostRadio.isSelected()) { + // if problematic new name for host + return HostNameValidator.getValidationMessage(specifyNewHostTextField.getText(), null, sanitizedHostSet); + + // or use existing host and no host is selected + } else if (useExistingHostRadio.isSelected() + && (existingHostList.getSelectedValue() == null + || existingHostList.getSelectedValue().getHost() == null)) { + return Bundle.AddImageWizardSelectHostVisual_getValidationMessage_noHostSelected(); + } + + return null; + } + + @Override + public String getName() { + return Bundle.AddImageWizardSelectHostVisual_title(); + } + + boolean hasValidData() { + return StringUtils.isBlank(validationMessage.getText()); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + javax.swing.ButtonGroup radioButtonGroup = new javax.swing.ButtonGroup(); + generateNewRadio = new javax.swing.JRadioButton(); + specifyNewHostRadio = new javax.swing.JRadioButton(); + specifyNewHostTextField = new javax.swing.JTextField(); + useExistingHostRadio = new javax.swing.JRadioButton(); + javax.swing.JScrollPane jScrollPane1 = new javax.swing.JScrollPane(); + existingHostList = new javax.swing.JList<>(); + hostDescription = new javax.swing.JLabel(); + validationMessage = new javax.swing.JLabel(); + + radioButtonGroup.add(generateNewRadio); + generateNewRadio.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(generateNewRadio, org.openide.util.NbBundle.getMessage(AddImageWizardSelectHostVisual.class, "AddImageWizardSelectHostVisual.generateNewRadio.text")); // NOI18N + generateNewRadio.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + generateNewRadioActionPerformed(evt); + } + }); + + radioButtonGroup.add(specifyNewHostRadio); + org.openide.awt.Mnemonics.setLocalizedText(specifyNewHostRadio, org.openide.util.NbBundle.getMessage(AddImageWizardSelectHostVisual.class, "AddImageWizardSelectHostVisual.specifyNewHostRadio.text")); // NOI18N + specifyNewHostRadio.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + specifyNewHostRadioActionPerformed(evt); + } + }); + + specifyNewHostTextField.setText(org.openide.util.NbBundle.getMessage(AddImageWizardSelectHostVisual.class, "AddImageWizardSelectHostVisual.specifyNewHostTextField.text")); // NOI18N + + radioButtonGroup.add(useExistingHostRadio); + org.openide.awt.Mnemonics.setLocalizedText(useExistingHostRadio, org.openide.util.NbBundle.getMessage(AddImageWizardSelectHostVisual.class, "AddImageWizardSelectHostVisual.useExistingHostRadio.text")); // NOI18N + useExistingHostRadio.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + useExistingHostRadioActionPerformed(evt); + } + }); + + existingHostList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + jScrollPane1.setViewportView(existingHostList); + + org.openide.awt.Mnemonics.setLocalizedText(hostDescription, org.openide.util.NbBundle.getMessage(AddImageWizardSelectHostVisual.class, "AddImageWizardSelectHostVisual.hostDescription.text")); // NOI18N + + validationMessage.setForeground(java.awt.Color.RED); + org.openide.awt.Mnemonics.setLocalizedText(validationMessage, org.openide.util.NbBundle.getMessage(AddImageWizardSelectHostVisual.class, "AddImageWizardSelectHostVisual.validationMessage.text")); // NOI18N + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(validationMessage, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(generateNewRadio) + .addComponent(useExistingHostRadio) + .addComponent(hostDescription) + .addGroup(layout.createSequentialGroup() + .addGap(21, 21, 21) + .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 270, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createSequentialGroup() + .addComponent(specifyNewHostRadio) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(specifyNewHostTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 270, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGap(0, 13, Short.MAX_VALUE))) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(hostDescription) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(generateNewRadio) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(specifyNewHostRadio) + .addComponent(specifyNewHostTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(useExistingHostRadio) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(10, 10, 10) + .addComponent(validationMessage) + .addContainerGap(18, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + + private void generateNewRadioActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_generateNewRadioActionPerformed + refresh(); + }//GEN-LAST:event_generateNewRadioActionPerformed + + private void specifyNewHostRadioActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_specifyNewHostRadioActionPerformed + refresh(); + }//GEN-LAST:event_specifyNewHostRadioActionPerformed + + private void useExistingHostRadioActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_useExistingHostRadioActionPerformed + refresh(); + }//GEN-LAST:event_useExistingHostRadioActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JList existingHostList; + private javax.swing.JRadioButton generateNewRadio; + private javax.swing.JLabel hostDescription; + private javax.swing.JRadioButton specifyNewHostRadio; + private javax.swing.JTextField specifyNewHostTextField; + private javax.swing.JRadioButton useExistingHostRadio; + private javax.swing.JLabel validationMessage; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties index 9019887a68..f3dfe72020 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties @@ -59,7 +59,7 @@ AddImageWizardChooseDataSourcePanel.moveFocusNext=Next > AddImageWizardChooseDataSourceVisual.getName.text=Select Data Source AddImageWizardIngestConfigPanel.dsProcDone.noErrs.text=*Data Source added. AddImageWizardIngestConfigPanel.dsProcDone.errs.text=*Errors encountered in adding Data Source. -AddImageWizardIngestConfigVisual.getName.text=Configure Ingest Modules +AddImageWizardIngestConfigVisual.getName.text=Configure Ingest AddImageWizardIterator.stepXofN=Step {0} of {1} AddLocalFilesTask.localFileAdd.progress.text=Adding: {0}/{1} Case.getCurCase.exception.noneOpen=Cannot get the current case; there is no case open\! @@ -258,3 +258,9 @@ SolrNotConfiguredDialog.okButton.text=OK SolrNotConfiguredDialog.title=Solr 8 Server Not Configured SolrNotConfiguredDialog.EmptyKeywordSearchHostName=Solr 8 connection parameters are not configured. Please go to Tools->Options->Multi User. SolrNotConfiguredDialog.messageLabel.text=Multi-User cases are enabled but Solr 8 server has not been configured.
\nNew cases can only be created with Solr 8. Please go to Tools->Options->Multi User.\n +AddImageWizardSelectHostVisual.hostDescription.text=Hosts are used to organize data sources and other data. +AddImageWizardSelectHostVisual.useExistingHostRadio.text=Use existing host +AddImageWizardSelectHostVisual.specifyNewHostTextField.text= +AddImageWizardSelectHostVisual.specifyNewHostRadio.text=Specify new host name +AddImageWizardSelectHostVisual.generateNewRadio.text=Generate new host name based on data source name +AddImageWizardSelectHostVisual.validationMessage.text=\ diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED index 30f782983e..f731913af5 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED @@ -1,5 +1,8 @@ -AddImageWizardIngestConfigPanel.name.text=Configure Ingest Modules +AddImageWizardIngestConfigPanel.name.text=Configure Ingest AddImageWizardSelectDspVisual.multiUserWarning.text=This type of Data Source Processor is not available in multi-user mode +AddImageWizardSelectHostPanel_title=Select Host To Add The Data Source To +AddImageWizardSelectHostVisual_getValidationMessage_noHostSelected=Please select an existing host. +AddImageWizardSelectHostVisual_title=Select Host # {0} - exception message Case.closeException.couldNotCloseCase=Error closing case: {0} Case.creationException.couldNotAcquireResourcesLock=Failed to get lock on case resources @@ -242,7 +245,7 @@ AddImageWizardChooseDataSourcePanel.moveFocusNext=Next > AddImageWizardChooseDataSourceVisual.getName.text=Select Data Source AddImageWizardIngestConfigPanel.dsProcDone.noErrs.text=*Data Source added. AddImageWizardIngestConfigPanel.dsProcDone.errs.text=*Errors encountered in adding Data Source. -AddImageWizardIngestConfigVisual.getName.text=Configure Ingest Modules +AddImageWizardIngestConfigVisual.getName.text=Configure Ingest AddImageWizardIterator.stepXofN=Step {0} of {1} AddLocalFilesTask.localFileAdd.progress.text=Adding: {0}/{1} Case.getCurCase.exception.noneOpen=Cannot get the current case; there is no case open\! @@ -341,7 +344,7 @@ RecentCases.exception.caseIdxOutOfRange.msg=Recent case index {0} is out of rang RecentCases.getName.text=Clear Recent Cases # {0} - case name RecentItems.openRecentCase.msgDlg.text=Case {0} no longer exists. -SelectDataSourceProcessorPanel.name.text=Select Type of Data Source To Add +SelectDataSourceProcessorPanel.name.text=Select Data Source Type StartupWindow.title.text=Welcome UnpackagePortableCaseDialog.title.text=Unpackage Portable Case UnpackagePortableCaseDialog.UnpackagePortableCaseDialog.extensions=Portable case package (.zip, .zip.001) @@ -476,3 +479,9 @@ SolrNotConfiguredDialog.okButton.text=OK SolrNotConfiguredDialog.title=Solr 8 Server Not Configured SolrNotConfiguredDialog.EmptyKeywordSearchHostName=Solr 8 connection parameters are not configured. Please go to Tools->Options->Multi User. SolrNotConfiguredDialog.messageLabel.text=Multi-User cases are enabled but Solr 8 server has not been configured.
\nNew cases can only be created with Solr 8. Please go to Tools->Options->Multi User.\n +AddImageWizardSelectHostVisual.hostDescription.text=Hosts are used to organize data sources and other data. +AddImageWizardSelectHostVisual.useExistingHostRadio.text=Use existing host +AddImageWizardSelectHostVisual.specifyNewHostTextField.text= +AddImageWizardSelectHostVisual.specifyNewHostRadio.text=Specify new host name +AddImageWizardSelectHostVisual.generateNewRadio.text=Generate new host name based on data source name +AddImageWizardSelectHostVisual.validationMessage.text=\ diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java index c2fd9d822f..0115b3a53c 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java @@ -81,6 +81,8 @@ import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.casemodule.events.DataSourceAddedEvent; import org.sleuthkit.autopsy.casemodule.events.DataSourceDeletedEvent; import org.sleuthkit.autopsy.casemodule.events.DataSourceNameChangedEvent; +import org.sleuthkit.autopsy.casemodule.events.OsAccountAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.OsAccountChangedEvent; import org.sleuthkit.autopsy.casemodule.events.ReportAddedEvent; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData.CaseNodeDataException; import org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils; @@ -104,6 +106,7 @@ import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.coreutils.ThreadUtils; import org.sleuthkit.autopsy.coreutils.TimeZoneUtils; import org.sleuthkit.autopsy.coreutils.Version; +import org.sleuthkit.autopsy.datamodel.hosts.OpenHostsAction; import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.events.AutopsyEventException; import org.sleuthkit.autopsy.events.AutopsyEventPublisher; @@ -128,6 +131,10 @@ import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.FileSystem; import org.sleuthkit.datamodel.Image; +import org.sleuthkit.datamodel.OsAccount; +import org.sleuthkit.datamodel.OsAccountManager; +import org.sleuthkit.datamodel.OsAccountManager.OsAccountsCreationEvent; +import org.sleuthkit.datamodel.OsAccountManager.OsAccountsUpdateEvent; import org.sleuthkit.datamodel.Report; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TimelineManager; @@ -409,7 +416,17 @@ public class Case { * An item in the central repository has had its comment modified. The * old value is null, the new value is string for current comment. */ - CR_COMMENT_CHANGED; + CR_COMMENT_CHANGED, + /** + * OSAccount associated with the current case added. Call getOsAccount + * to get the added account; + */ + OS_ACCOUNT_ADDED, + /** + * OSAccount associated with the current case has changed. + * Call getOsAccount to get the changed account; + */ + OS_ACCOUNT_CHANGED; }; @@ -443,6 +460,20 @@ public class Case { event.getArtifacts(artifactType))); } } + + @Subscribe + public void publishOsAccountAddedEvent(OsAccountsCreationEvent event) { + for(OsAccount account: event.getOsAcounts()) { + eventPublisher.publish(new OsAccountAddedEvent(account)); + } + } + + @Subscribe + public void publishOsAccountChangedEvent(OsAccountsUpdateEvent event) { + for(OsAccount account: event.getOsAcounts()) { + eventPublisher.publish(new OsAccountChangedEvent(account)); + } + } } /** @@ -1111,6 +1142,7 @@ public class Case { * Enable the case-specific actions. */ CallableSystemAction.get(AddImageAction.class).setEnabled(FeatureAccessUtils.canAddDataSources()); + CallableSystemAction.get(OpenHostsAction.class).setEnabled(true); CallableSystemAction.get(CaseCloseAction.class).setEnabled(true); CallableSystemAction.get(CaseDetailsAction.class).setEnabled(true); CallableSystemAction.get(DataSourceSummaryAction.class).setEnabled(true); @@ -1166,6 +1198,7 @@ public class Case { * Disable the case-specific menu items. */ CallableSystemAction.get(AddImageAction.class).setEnabled(false); + CallableSystemAction.get(OpenHostsAction.class).setEnabled(false); CallableSystemAction.get(CaseCloseAction.class).setEnabled(false); CallableSystemAction.get(CaseDetailsAction.class).setEnabled(false); CallableSystemAction.get(DataSourceSummaryAction.class).setEnabled(false); @@ -1639,7 +1672,15 @@ public class Case { public void notifyBlackBoardArtifactTagDeleted(BlackboardArtifactTag deletedTag) { eventPublisher.publish(new BlackBoardArtifactTagDeletedEvent(deletedTag)); } + + public void notifyOsAccountAdded(OsAccount account) { + eventPublisher.publish(new OsAccountAddedEvent(account)); + } + public void notifyOsAccountChanged(OsAccount account) { + eventPublisher.publish(new OsAccountChangedEvent(account)); + } + /** * Adds a report to the case. * diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java index bb7e6ac469..92335e75b1 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java @@ -268,14 +268,6 @@ public class ImageDSProcessor implements DataSourceProcessor, AutoIngestDataSour // Read the settings from the wizard readConfigSettings(); this.host = host; - - // HOSTTODO - remove once passing in a host - try { - this.host = Case.getCurrentCase().getSleuthkitCase().getHostManager().getOrCreateHost("ImageDSProcessor Host"); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error creating/loading host", ex); - this.host = null; - } // Set up the data source before creating the ingest stream try { diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java index b34a21ab1d..6cf15c54f7 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java @@ -171,13 +171,6 @@ public class LocalDiskDSProcessor implements DataSourceProcessor { } this.host = host; - // HOSTTODO - set to value from config panel - try { - this.host = Case.getCurrentCase().getSleuthkitCase().getHostManager().getOrCreateHost("LocalDiskDSProcessor Host"); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error creating/loading host", ex); - this.host = null; - } Image image; try { diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java index 41be66f0da..dab8ea19ec 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java @@ -177,14 +177,6 @@ public class LocalFilesDSProcessor implements DataSourceProcessor, AutoIngestDat public void run(Host host, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { if (!setDataSourceOptionsCalled) { - // HOSTTODO - use passed in value - try { - host = Case.getCurrentCase().getSleuthkitCase().getHostManager().getOrCreateHost("LocalFilesDSProcessor Host"); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error creating/loading host", ex); - host = null; - } - localFilePaths = configPanel.getContentPaths(); if (configPanel.subTypeIsLogicalEvidencePanel()) { try { diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/events/OsAccountAddedEvent.java b/Core/src/org/sleuthkit/autopsy/casemodule/events/OsAccountAddedEvent.java new file mode 100755 index 0000000000..c9f89903f4 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/events/OsAccountAddedEvent.java @@ -0,0 +1,35 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.casemodule.events; + +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.datamodel.OsAccount; + +/** + * Event published when an OsAccount is added to a case. + */ +public final class OsAccountAddedEvent extends OsAccountEvent { + + private static final long serialVersionUID = 1L; + + public OsAccountAddedEvent(OsAccount account) { + super(Case.Events.OS_ACCOUNT_ADDED.toString(), account); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/events/OsAccountChangedEvent.java b/Core/src/org/sleuthkit/autopsy/casemodule/events/OsAccountChangedEvent.java new file mode 100755 index 0000000000..237373a8b9 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/events/OsAccountChangedEvent.java @@ -0,0 +1,34 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.casemodule.events; + +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.datamodel.OsAccount; + +/** + * Event published when an OsAccount is updated. + */ +public final class OsAccountChangedEvent extends OsAccountEvent { + + private static final long serialVersionUID = 1L; + + public OsAccountChangedEvent(OsAccount account) { + super(Case.Events.OS_ACCOUNT_CHANGED.toString(), account); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/events/OsAccountEvent.java b/Core/src/org/sleuthkit/autopsy/casemodule/events/OsAccountEvent.java new file mode 100755 index 0000000000..22b0622ba8 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/events/OsAccountEvent.java @@ -0,0 +1,64 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.casemodule.events; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.sleuthkit.datamodel.OsAccount; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Parent class for specific OsAccount event classes. + */ +class OsAccountEvent extends TskDataModelChangeEvent { + + private static final long serialVersionUID = 1L; + + /** + * Construct a new OsAccountEvent. + * + * @param eventName The name of the event. + * @param account The OsAccount the event applies to. + */ + OsAccountEvent(String eventName, OsAccount account) { + super(eventName, Stream.of(account.getId()).collect(Collectors.toList()), Stream.of(account).collect(Collectors.toList())); + } + + /** + * Returns the OsAccount that changed. + * + * @return The OsAccount that was changed. + */ + public OsAccount getOsAccount() { + List accounts = getNewValue(); + return accounts.get(0); + } + + @Override + protected List getDataModelObjects(SleuthkitCase caseDb, List ids) throws TskCoreException { + Long id = ids.get(0); + OsAccount account = caseDb.getOsAccountManager().getOsAccount(id); + List accounts = new ArrayList<>(); + accounts.add(account); + return accounts; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/events/TskDataModelChangeEvent.java b/Core/src/org/sleuthkit/autopsy/casemodule/events/TskDataModelChangeEvent.java index 891aed0f75..1bd9fd81ac 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/events/TskDataModelChangeEvent.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/events/TskDataModelChangeEvent.java @@ -75,8 +75,8 @@ public abstract class TskDataModelChangeEvent extends AutopsyEvent { * * @return The unique IDs. */ - public final List getDataModelObjectIds() { - return Collections.unmodifiableList(dataModelObjects); + public final List getDataModelObjectIds() { + return Collections.unmodifiableList(dataModelObjectIds); } /** diff --git a/Core/src/org/sleuthkit/autopsy/core/layer.xml b/Core/src/org/sleuthkit/autopsy/core/layer.xml index 288c25ff3a..923e8f5c5e 100644 --- a/Core/src/org/sleuthkit/autopsy/core/layer.xml +++ b/Core/src/org/sleuthkit/autopsy/core/layer.xml @@ -49,6 +49,7 @@ + @@ -164,7 +165,11 @@ - + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/OsAccounts.java b/Core/src/org/sleuthkit/autopsy/datamodel/OsAccounts.java index faf3cfd0ae..ade1ef6329 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/OsAccounts.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/OsAccounts.java @@ -18,9 +18,14 @@ */ package org.sleuthkit.autopsy.datamodel; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.logging.Level; import org.openide.nodes.ChildFactory; import org.openide.nodes.Children; @@ -28,6 +33,8 @@ import org.openide.nodes.Node; import org.openide.nodes.Sheet; import org.openide.util.NbBundle.Messages; import org.openide.util.lookup.Lookups; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.events.OsAccountChangedEvent; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.Host; import org.sleuthkit.datamodel.OsAccount; @@ -99,8 +106,25 @@ public final class OsAccounts implements AutopsyVisitableItem { * The child node factory that creates the OsAccountNode children for a * OsAccountListNode. */ - private final class OsAccountNodeFactory extends ChildFactory { + private final class OsAccountNodeFactory extends ChildFactory.Detachable { + private final PropertyChangeListener listener = new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + refresh(true); + } + }; + + @Override + protected void addNotify() { + Case.addEventTypeSubscriber(Collections.singleton(Case.Events.OS_ACCOUNT_ADDED), listener); + } + + @Override + protected void removeNotify() { + Case.removeEventTypeSubscriber(Collections.singleton(Case.Events.OS_ACCOUNT_ADDED), listener); + } + @Override protected boolean createKeys(List list) { try { @@ -129,7 +153,18 @@ public final class OsAccounts implements AutopsyVisitableItem { */ public static final class OsAccountNode extends DisplayableItemNode { - private final OsAccount account; + private OsAccount account; + + private final PropertyChangeListener listener = new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + if(((OsAccountChangedEvent)evt).getOsAccount().getId() == account.getId()) { + // Update the account node to the new one + account = ((OsAccountChangedEvent)evt).getOsAccount(); + updateSheet(); + } + } + }; /** * Constructs a new OsAccountNode. @@ -143,6 +178,8 @@ public final class OsAccounts implements AutopsyVisitableItem { setName(account.getName()); setDisplayName(account.getName()); setIconBaseWithExtension(ICON_PATH); + + Case.addEventTypeSubscriber(Collections.singleton(Case.Events.OS_ACCOUNT_CHANGED), listener); } @Override @@ -159,7 +196,7 @@ public final class OsAccounts implements AutopsyVisitableItem { public String getItemType() { return getClass().getName(); } - + @Messages({ "OsAccounts_accountNameProperty_name=Name", "OsAccounts_accountNameProperty_displayName=Name", @@ -174,6 +211,13 @@ public final class OsAccounts implements AutopsyVisitableItem { "OsAccounts_loginNameProperty_displayName=Login Name", "OsAccounts_loginNameProperty_desc=Os Account login name" }) + + /** + * Refreshes this node's property sheet. + */ + void updateSheet() { + this.setSheet(createSheet()); + } @Override protected Sheet createSheet() { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/hosts/AddEditHostDialog.form b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/AddEditHostDialog.form new file mode 100644 index 0000000000..36ed76d8f5 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/AddEditHostDialog.form @@ -0,0 +1,117 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/hosts/AddEditHostDialog.java b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/AddEditHostDialog.java new file mode 100644 index 0000000000..4a02edc69a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/AddEditHostDialog.java @@ -0,0 +1,230 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 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.datamodel.hosts; + +import java.awt.Color; +import org.sleuthkit.datamodel.Host; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.openide.util.NbBundle.Messages; + +/** + * + * Dialog for adding or editing a host. + */ +class AddEditHostDialog extends javax.swing.JDialog { + + private static final long serialVersionUID = 1L; + + private boolean changed = false; + + // host names to upper and trimmed + private final Set hostNamesSanitized; + private final Host initialHost; + + /** + * Main constructor. + * + * @param parent The parent frame for this dialog. + * @param currentHosts The current set of hosts in the case. + */ + AddEditHostDialog(java.awt.Frame parent, Collection currentHosts) { + this(parent, currentHosts, null); + } + + /** + * Main constructor. + * + * @param parent The parent frame for this dialog. + * @param currentHosts The current set of hosts (used for determining if + * name is unique). + * @param initialHost If adding a new host, this will be a null value. + * Otherwise, if editing, this will be the host being edited. + */ + @Messages({ + "AddEditHostDialog_addHost_title=Add Host", + "AddEditHostDialog_editHost_title=Edit Host" + }) + AddEditHostDialog(java.awt.Frame parent, Collection currentHosts, Host initialHost) { + super(parent, true); + this.initialHost = initialHost; + setTitle(initialHost == null ? Bundle.AddEditHostDialog_addHost_title() : Bundle.AddEditHostDialog_editHost_title()); + + hostNamesSanitized = HostNameValidator.getSanitizedHostNames(currentHosts); + + initComponents(); + onNameUpdate(initialHost == null ? null : initialHost.getName()); + + // initially, don't show validation message (for empty strings or repeat), + // but do disable ok button if not valid. + validationLabel.setText(""); + + inputTextField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void changedUpdate(DocumentEvent e) { + onNameUpdate(inputTextField.getText()); + } + + @Override + public void removeUpdate(DocumentEvent e) { + onNameUpdate(inputTextField.getText()); + } + + @Override + public void insertUpdate(DocumentEvent e) { + onNameUpdate(inputTextField.getText()); + } + }); + } + + /** + * @return The string value for the name in the input field if Ok pressed or + * null if not. + */ + String getValue() { + return inputTextField.getText(); + } + + /** + * @return Whether or not the value has been changed and the user pressed + * okay to save the new value. + */ + boolean isChanged() { + return changed; + } + + /** + * When the text field is updated, this method is called. + * + * @param newNameValue + */ + private void onNameUpdate(String newNameValue) { + String newNameValueOrEmpty = newNameValue == null ? "" : newNameValue; + // update input text field if it is not the same. + if (!newNameValueOrEmpty.equals(this.inputTextField.getText())) { + inputTextField.setText(newNameValue); + } + + // validate text input against invariants setting validation + // message and whether or not okay button is enabled accordingly. + String validationMessage = HostNameValidator.getValidationMessage( + newNameValue, initialHost == null ? null : initialHost.getName(), hostNamesSanitized); + + okButton.setEnabled(validationMessage == null); + validationLabel.setText(validationMessage == null ? "" : validationMessage); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + inputTextField = new javax.swing.JTextField(); + javax.swing.JLabel nameLabel = new javax.swing.JLabel(); + validationLabel = new javax.swing.JLabel(); + okButton = new javax.swing.JButton(); + javax.swing.JButton cancelButton = new javax.swing.JButton(); + + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + + inputTextField.setText(org.openide.util.NbBundle.getMessage(AddEditHostDialog.class, "AddEditHostDialog.inputTextField.text")); // NOI18N + + nameLabel.setText(org.openide.util.NbBundle.getMessage(AddEditHostDialog.class, "AddEditHostDialog.nameLabel.text")); // NOI18N + + validationLabel.setForeground(Color.RED); + validationLabel.setVerticalAlignment(javax.swing.SwingConstants.TOP); + + okButton.setText(org.openide.util.NbBundle.getMessage(AddEditHostDialog.class, "AddEditHostDialog.okButton.text")); // NOI18N + okButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + okButtonActionPerformed(evt); + } + }); + + cancelButton.setText(org.openide.util.NbBundle.getMessage(AddEditHostDialog.class, "AddEditHostDialog.cancelButton.text")); // NOI18N + cancelButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cancelButtonActionPerformed(evt); + } + }); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(validationLabel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(inputTextField) + .addGroup(layout.createSequentialGroup() + .addComponent(nameLabel) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGap(0, 288, Short.MAX_VALUE) + .addComponent(okButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cancelButton))) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(nameLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(inputTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(validationLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 18, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(cancelButton) + .addComponent(okButton)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + + pack(); + }// //GEN-END:initComponents + + private void okButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okButtonActionPerformed + this.changed = true; + dispose(); + }//GEN-LAST:event_okButtonActionPerformed + + + private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed + this.changed = false; + dispose(); + }//GEN-LAST:event_cancelButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JTextField inputTextField; + private javax.swing.JButton okButton; + private javax.swing.JLabel validationLabel; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/hosts/Bundle.properties b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/Bundle.properties new file mode 100644 index 0000000000..1c2607cb95 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/Bundle.properties @@ -0,0 +1,15 @@ +# 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. +ManageHostsDialog.hostDetailsLabel.text=Host Details +ManageHostsDialog.hostNameLabel.text=Host Name: +ManageHostsDialog.closeButton.text=Close +ManageHostsDialog.hostDescriptionTextArea.text=Hosts represent individual devices that may have multiple data sources. +ManageHostsDialog.hostListLabel.text=Hosts +ManageHostsDialog.newButton.text=New +ManageHostsDialog.editButton.text=Edit +ManageHostsDialog.deleteButton.text=Delete +AddEditHostDialog.nameLabel.text=Name: +AddEditHostDialog.okButton.text=OK +AddEditHostDialog.cancelButton.text=Cancel +AddEditHostDialog.inputTextField.text=jTextField1 diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/hosts/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/Bundle.properties-MERGED new file mode 100644 index 0000000000..22dc1165b3 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/Bundle.properties-MERGED @@ -0,0 +1,23 @@ +AddEditHostDialog_addHost_title=Add Host +AddEditHostDialog_editHost_title=Edit Host +CTL_OpenHosts=Manage Hosts +HostNameValidator_getValidationMessage_onDuplicate=Another host already has the same name. Please choose a different name. +HostNameValidator_getValidationMessage_onEmpty=Please provide some text for the host name. +HostNameValidator_getValidationMessage_sameAsOriginal=Please provide a new name for this host. +# 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. +ManageHostsDialog.hostDetailsLabel.text=Host Details +ManageHostsDialog.hostNameLabel.text=Host Name: +ManageHostsDialog.closeButton.text=Close +ManageHostsDialog.hostDescriptionTextArea.text=Hosts represent individual devices that may have multiple data sources. +ManageHostsDialog.hostListLabel.text=Hosts +ManageHostsDialog.newButton.text=New +ManageHostsDialog.editButton.text=Edit +ManageHostsDialog.deleteButton.text=Delete +AddEditHostDialog.nameLabel.text=Name: +AddEditHostDialog.okButton.text=OK +AddEditHostDialog.cancelButton.text=Cancel +AddEditHostDialog.inputTextField.text=jTextField1 +ManageHostsDialog_title_text=Manage Hosts +OpenHostsAction_displayName=Hosts diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/hosts/HostNameValidator.java b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/HostNameValidator.java new file mode 100644 index 0000000000..a5790af6e7 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/HostNameValidator.java @@ -0,0 +1,81 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 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.datamodel.hosts; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.openide.util.NbBundle; +import org.sleuthkit.datamodel.Host; + +/** + * Provides methods for validating host names. + */ +public class HostNameValidator { + + /** + * Gets the validation message based on the current text checked against the + * host names. + * + * @param curName The current name in the text field. + * @param initialName If editing a name, the initial name of the host. + * Otherwise, null can be provided for this parameter. + * @param currentHostsTrimmedUpper The current host names. This set should + * be sanitized to upper case and trimmed. + * @return The validation message if the name is not valid or null. + */ + @NbBundle.Messages({ + "HostNameValidator_getValidationMessage_onEmpty=Please provide some text for the host name.", + "HostNameValidator_getValidationMessage_sameAsOriginal=Please provide a new name for this host.", + "HostNameValidator_getValidationMessage_onDuplicate=Another host already has the same name. Please choose a different name.",}) + public static String getValidationMessage(String curName, String initialName, Set currentHostsTrimmedUpper) { + + if (StringUtils.isBlank(curName)) { + return Bundle.HostNameValidator_getValidationMessage_onEmpty(); + } + + if (StringUtils.equalsIgnoreCase(initialName, curName)) { + return Bundle.HostNameValidator_getValidationMessage_sameAsOriginal(); + } + + if (currentHostsTrimmedUpper.contains(curName.trim().toUpperCase())) { + return Bundle.HostNameValidator_getValidationMessage_onDuplicate(); + } + + return null; + } + + /** + * Generates a list of host names trimmed and to upper case that can be used + * with getValidationMessage. + * + * @param hosts The hosts. + * @return The set of host names trimmed and to upper case. + */ + public static Set getSanitizedHostNames(Collection hosts) { + Stream hostsStream = hosts != null ? hosts.stream() : Stream.empty(); + return hostsStream + .map(h -> h == null ? null : h.getName()) + .filter(hName -> StringUtils.isNotBlank(hName)) + .map(hName -> hName.trim().toUpperCase()) + .collect(Collectors.toSet()); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/hosts/ManageHostsDialog.form b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/ManageHostsDialog.form new file mode 100644 index 0000000000..f01b0cbc8c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/ManageHostsDialog.form @@ -0,0 +1,359 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/hosts/ManageHostsDialog.java b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/ManageHostsDialog.java new file mode 100644 index 0000000000..6d27cd53a1 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/ManageHostsDialog.java @@ -0,0 +1,580 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 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.datamodel.hosts; + +import java.awt.Dialog; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Vector; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javax.swing.JFrame; +import javax.swing.ListModel; +import org.apache.commons.collections4.CollectionUtils; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.Host; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Dialog for managing CRUD operations with hosts from the UI. + */ +@Messages({ + "ManageHostsDialog_title_text=Manage Hosts" +}) +public class ManageHostsDialog extends javax.swing.JDialog { + + /** + * List item to be used with jlist. + */ + private static class HostListItem { + + private final Host host; + private final List dataSources; + + /** + * Main constructor. + * + * @param host The host. + * @param dataSources The data sources that are children of this host. + */ + HostListItem(Host host, List dataSources) { + this.host = host; + this.dataSources = dataSources; + } + + /** + * @return The host. + */ + Host getHost() { + return host; + } + + /** + * @return The data sources associated with this host. + */ + List getDataSources() { + return dataSources; + } + + @Override + public String toString() { + return host == null ? "" : host.getName(); + } + + @Override + public int hashCode() { + int hash = 5; + hash = 89 * hash + Objects.hashCode(this.host == null ? 0 : this.host.getId()); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final HostListItem other = (HostListItem) obj; + if (this.host == null || other.getHost() == null) { + return this.host == null && other.getHost() == null; + } + + return this.host.getId() == other.getHost().getId(); + } + + } + + private static final Logger logger = Logger.getLogger(ManageHostsDialog.class.getName()); + private static final long serialVersionUID = 1L; + + private Map> hostChildrenMap = Collections.emptyMap(); + + /** + * Main constructor. + * + * @param parent The parent frame. + */ + public ManageHostsDialog(java.awt.Frame parent) { + super(parent, Bundle.ManageHostsDialog_title_text(), true); + init(); + } + + /** + * Main constructor. + * + * @param parent The parent dialog. + */ + public ManageHostsDialog(Dialog parent) { + super(parent, Bundle.ManageHostsDialog_title_text(), true); + init(); + } + + /** + * Initializes components, loads host data, and sets up list listener. + */ + private void init() { + initComponents(); + refresh(); + + // refreshes UI when selection changes including button enabled state and data. + this.hostList.addListSelectionListener((evt) -> refreshComponents()); + } + + /** + * @return The currently selected host in the list or null if no host is + * selected. + */ + Host getSelectedHost() { + return (hostList.getSelectedValue() == null) ? null : hostList.getSelectedValue().getHost(); + } + + /** + * Shows add/edit dialog, and if a value is returned, creates a new Host. + */ + private void addHost() { + String newHostName = getAddEditDialogName(null); + if (newHostName != null) { + Long selectedId = null; + try { + Host newHost = Case.getCurrentCaseThrows().getSleuthkitCase().getHostManager().createHost(newHostName); + selectedId = newHost == null ? null : newHost.getId(); + } catch (NoCurrentCaseException | TskCoreException e) { + logger.log(Level.WARNING, String.format("Unable to add new host '%s' at this time.", newHostName), e); + } + refresh(); + setSelectedHostById(selectedId); + } + } + + /** + * Deletes the selected host if possible. + * + * @param selectedHost + */ + private void deleteHost(Host selectedHost) { + if (selectedHost != null && selectedHost.getName() != null) { + try { + Case.getCurrentCaseThrows().getSleuthkitCase().getHostManager().deleteHost(selectedHost.getName()); + } catch (NoCurrentCaseException | TskCoreException e) { + logger.log(Level.WARNING, String.format("Unable to delete host '%s' at this time.", selectedHost.getName()), e); + } + refresh(); + } + } + + /** + * Selects the host with the given id. If no matching id found in list. + * + * @param selectedId The id of the host to select. + */ + private void setSelectedHostById(Long selectedId) { + ListModel model = hostList.getModel(); + + if (selectedId == null) { + hostList.clearSelection(); + } + + for (int i = 0; i < model.getSize(); i++) { + Object o = model.getElementAt(i); + if (!(o instanceof HostListItem)) { + continue; + } + + Host host = ((HostListItem) o).getHost(); + if (host == null) { + continue; + } + + if (host.getId() == selectedId) { + hostList.setSelectedIndex(i); + return; + } + } + + hostList.clearSelection(); + } + + /** + * Shows add/edit dialog, and if a value is returned, creates a new Host. + * + * @param selectedHost The selected host. + */ + private void editHost(Host selectedHost) { + + if (selectedHost != null) { + String newHostName = getAddEditDialogName(selectedHost); + if (newHostName != null) { + selectedHost.setName(newHostName); + try { + Case.getCurrentCaseThrows().getSleuthkitCase().getHostManager().updateHost(selectedHost); + } catch (NoCurrentCaseException | TskCoreException e) { + logger.log(Level.WARNING, String.format("Unable to update host '%s' with id: %d at this time.", selectedHost.getName(), selectedHost.getId()), e); + } + + HostListItem selectedItem = hostList.getSelectedValue(); + Long selectedId = selectedItem == null || selectedItem.getHost() == null ? null : selectedItem.getHost().getId(); + + refresh(); + + setSelectedHostById(selectedId); + } + } + } + + /** + * Shows the dialog to add or edit the name of a host. + * + * @param origValue The original values for the host or null if adding a + * host. + * @return The new name for the host or null if operation was cancelled. + */ + private String getAddEditDialogName(Host origValue) { + JFrame parent = (this.getRootPane() != null && this.getRootPane().getParent() instanceof JFrame) + ? (JFrame) this.getRootPane().getParent() + : null; + + AddEditHostDialog addEditDialog = new AddEditHostDialog(parent, hostChildrenMap.keySet(), origValue); + addEditDialog.setResizable(false); + addEditDialog.setLocationRelativeTo(parent); + addEditDialog.setVisible(true); + addEditDialog.toFront(); + + if (addEditDialog.isChanged()) { + String newHostName = addEditDialog.getValue(); + return newHostName; + } + + return null; + } + + /** + * Refreshes the data and ui components for this dialog. + */ + private void refresh() { + refreshData(); + refreshComponents(); + } + + /** + * Refreshes the data for this dialog and updates the host JList with the + * hosts. + */ + private void refreshData() { + + hostChildrenMap = getHostListData(); + + Vector jlistData = hostChildrenMap.entrySet().stream() + .sorted((a, b) -> getNameOrEmpty(a.getKey()).compareTo(getNameOrEmpty(b.getKey()))) + .map(entry -> new HostListItem(entry.getKey(), entry.getValue())) + .collect(Collectors.toCollection(Vector::new)); + + hostList.setListData(jlistData); + } + + /** + * Returns the name of the host or an empty string if the host or name of + * host is null. + * + * @param h The host. + * @return The name of the host or empty string. + */ + private String getNameOrEmpty(Host h) { + return (h == null || h.getName() == null) ? "" : h.getName(); + } + + /** + * Retrieves the current list of hosts for the case. + * + * @return The list of hosts to be displayed in the list (sorted + * alphabetically). + */ + private Map> getHostListData() { + Map> hostMapping = new HashMap<>(); + try { + SleuthkitCase curCase = Case.getCurrentCaseThrows().getSleuthkitCase(); + List hosts = curCase.getHostManager().getHosts(); + List dataSources = curCase.getDataSources(); + + if (dataSources != null) { + for (DataSource ds : dataSources) { + List hostDataSources = hostMapping.computeIfAbsent(ds.getHost(), (d) -> new ArrayList<>()); + hostDataSources.add(ds); + } + + } + + if (hosts != null) { + for (Host host : hosts) { + hostMapping.putIfAbsent(host, Collections.emptyList()); + } + } + + } catch (TskCoreException | NoCurrentCaseException ex) { + logger.log(Level.WARNING, "There was an error while fetching hosts for current case.", ex); + } + + return hostMapping; + } + + /** + * Refreshes component's enabled state and displayed host data. + */ + private void refreshComponents() { + HostListItem selectedItem = hostList.getSelectedValue(); + Host selectedHost = selectedItem == null ? null : selectedItem.getHost(); + List dataSources = selectedItem == null ? null : selectedItem.getDataSources(); + this.editButton.setEnabled(selectedHost != null); + this.deleteButton.setEnabled(selectedHost != null && CollectionUtils.isEmpty(dataSources)); + String nameTextFieldStr = selectedHost != null && selectedHost.getName() != null ? selectedHost.getName() : ""; + this.hostNameTextField.setText(nameTextFieldStr); + + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + javax.swing.JScrollPane manageHostsScrollPane = new javax.swing.JScrollPane(); + javax.swing.JPanel manageHostsPanel = new javax.swing.JPanel(); + javax.swing.JScrollPane hostListScrollPane = new javax.swing.JScrollPane(); + hostList = new javax.swing.JList<>(); + javax.swing.JScrollPane hostDescriptionScrollPane = new javax.swing.JScrollPane(); + hostDescriptionTextArea = new javax.swing.JTextArea(); + newButton = new javax.swing.JButton(); + deleteButton = new javax.swing.JButton(); + closeButton = new javax.swing.JButton(); + javax.swing.JLabel hostListLabel = new javax.swing.JLabel(); + javax.swing.JSeparator jSeparator1 = new javax.swing.JSeparator(); + javax.swing.JLabel hostNameLabel = new javax.swing.JLabel(); + hostNameTextField = new javax.swing.JTextField(); + editButton = new javax.swing.JButton(); + javax.swing.JLabel hostDetailsLabel = new javax.swing.JLabel(); + + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + setMinimumSize(new java.awt.Dimension(600, 450)); + + manageHostsScrollPane.setMinimumSize(new java.awt.Dimension(600, 450)); + manageHostsScrollPane.setPreferredSize(new java.awt.Dimension(600, 450)); + + manageHostsPanel.setPreferredSize(new java.awt.Dimension(527, 407)); + + hostList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + hostListScrollPane.setViewportView(hostList); + + hostDescriptionTextArea.setEditable(false); + hostDescriptionTextArea.setBackground(new java.awt.Color(240, 240, 240)); + hostDescriptionTextArea.setColumns(20); + hostDescriptionTextArea.setLineWrap(true); + hostDescriptionTextArea.setRows(3); + hostDescriptionTextArea.setText(org.openide.util.NbBundle.getMessage(ManageHostsDialog.class, "ManageHostsDialog.hostDescriptionTextArea.text")); // NOI18N + hostDescriptionTextArea.setWrapStyleWord(true); + hostDescriptionScrollPane.setViewportView(hostDescriptionTextArea); + + newButton.setText(org.openide.util.NbBundle.getMessage(ManageHostsDialog.class, "ManageHostsDialog.newButton.text")); // NOI18N + newButton.setMargin(new java.awt.Insets(2, 6, 2, 6)); + newButton.setMaximumSize(new java.awt.Dimension(70, 23)); + newButton.setMinimumSize(new java.awt.Dimension(70, 23)); + newButton.setPreferredSize(new java.awt.Dimension(70, 23)); + newButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + newButtonActionPerformed(evt); + } + }); + + deleteButton.setText(org.openide.util.NbBundle.getMessage(ManageHostsDialog.class, "ManageHostsDialog.deleteButton.text")); // NOI18N + deleteButton.setMargin(new java.awt.Insets(2, 6, 2, 6)); + deleteButton.setMaximumSize(new java.awt.Dimension(70, 23)); + deleteButton.setMinimumSize(new java.awt.Dimension(70, 23)); + deleteButton.setPreferredSize(new java.awt.Dimension(70, 23)); + deleteButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + deleteButtonActionPerformed(evt); + } + }); + + closeButton.setText(org.openide.util.NbBundle.getMessage(ManageHostsDialog.class, "ManageHostsDialog.closeButton.text")); // NOI18N + closeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + closeButtonActionPerformed(evt); + } + }); + + hostListLabel.setText(org.openide.util.NbBundle.getMessage(ManageHostsDialog.class, "ManageHostsDialog.hostListLabel.text")); // NOI18N + + jSeparator1.setOrientation(javax.swing.SwingConstants.VERTICAL); + + hostNameLabel.setText(org.openide.util.NbBundle.getMessage(ManageHostsDialog.class, "ManageHostsDialog.hostNameLabel.text")); // NOI18N + + hostNameTextField.setEditable(false); + + editButton.setText(org.openide.util.NbBundle.getMessage(ManageHostsDialog.class, "ManageHostsDialog.editButton.text")); // NOI18N + editButton.setMaximumSize(new java.awt.Dimension(70, 23)); + editButton.setMinimumSize(new java.awt.Dimension(70, 23)); + editButton.setPreferredSize(new java.awt.Dimension(70, 23)); + editButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + editButtonActionPerformed(evt); + } + }); + + hostDetailsLabel.setText(org.openide.util.NbBundle.getMessage(ManageHostsDialog.class, "ManageHostsDialog.hostDetailsLabel.text")); // NOI18N + + javax.swing.GroupLayout manageHostsPanelLayout = new javax.swing.GroupLayout(manageHostsPanel); + manageHostsPanel.setLayout(manageHostsPanelLayout); + manageHostsPanelLayout.setHorizontalGroup( + manageHostsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(manageHostsPanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(manageHostsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(hostDescriptionScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 225, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(hostListLabel) + .addGroup(manageHostsPanelLayout.createSequentialGroup() + .addComponent(newButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(editButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(deleteButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(hostListScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 224, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(manageHostsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(manageHostsPanelLayout.createSequentialGroup() + .addGroup(manageHostsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(manageHostsPanelLayout.createSequentialGroup() + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(closeButton)) + .addGroup(manageHostsPanelLayout.createSequentialGroup() + .addGap(29, 29, 29) + .addComponent(hostNameLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(hostNameTextField, javax.swing.GroupLayout.DEFAULT_SIZE, 79, Short.MAX_VALUE))) + .addContainerGap()) + .addGroup(manageHostsPanelLayout.createSequentialGroup() + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(hostDetailsLabel) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))) + ); + manageHostsPanelLayout.setVerticalGroup( + manageHostsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(manageHostsPanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(manageHostsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(manageHostsPanelLayout.createSequentialGroup() + .addComponent(hostDetailsLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(manageHostsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(hostNameLabel) + .addComponent(hostNameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(closeButton)) + .addComponent(jSeparator1) + .addGroup(manageHostsPanelLayout.createSequentialGroup() + .addComponent(hostDescriptionScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(hostListLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(hostListScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 325, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(manageHostsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(newButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(deleteButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(editButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))) + .addContainerGap()) + ); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("org/sleuthkit/autopsy/datamodel/hosts/Bundle"); // NOI18N + newButton.getAccessibleContext().setAccessibleName(bundle.getString("ManageHostsDialog.newButton.text")); // NOI18N + deleteButton.getAccessibleContext().setAccessibleName(bundle.getString("ManageHostsDialog.deleteButton.text")); // NOI18N + closeButton.getAccessibleContext().setAccessibleName(bundle.getString("ManageHostsDialog.closeButton.text")); // NOI18N + hostListLabel.getAccessibleContext().setAccessibleName(bundle.getString("ManageHostsDialog.hostListLabel.text")); // NOI18N + hostNameLabel.getAccessibleContext().setAccessibleName(bundle.getString("ManageHostsDialog.hostNameLabel.text")); // NOI18N + editButton.getAccessibleContext().setAccessibleName(bundle.getString("ManageHostsDialog.editButton.text")); // NOI18N + hostDetailsLabel.getAccessibleContext().setAccessibleName(bundle.getString("ManageHostsDialog.hostDetailsLabel.text")); // NOI18N + + manageHostsScrollPane.setViewportView(manageHostsPanel); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGap(0, 0, 0) + .addComponent(manageHostsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGap(0, 0, 0) + .addComponent(manageHostsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + + pack(); + }// //GEN-END:initComponents + + private void newButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_newButtonActionPerformed + addHost(); + }//GEN-LAST:event_newButtonActionPerformed + + private void deleteButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deleteButtonActionPerformed + HostListItem listItem = this.hostList.getSelectedValue(); + if (listItem != null && listItem.getHost() != null) { + deleteHost(listItem.getHost()); + } + }//GEN-LAST:event_deleteButtonActionPerformed + + private void closeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_closeButtonActionPerformed + dispose(); + }//GEN-LAST:event_closeButtonActionPerformed + + private void editButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_editButtonActionPerformed + HostListItem listItem = this.hostList.getSelectedValue(); + if (listItem != null && listItem.getHost() != null) { + editHost(listItem.getHost()); + } + }//GEN-LAST:event_editButtonActionPerformed + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton closeButton; + private javax.swing.JButton deleteButton; + private javax.swing.JButton editButton; + private javax.swing.JTextArea hostDescriptionTextArea; + private javax.swing.JList hostList; + private javax.swing.JTextField hostNameTextField; + private javax.swing.JButton newButton; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/hosts/OpenHostsAction.java b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/OpenHostsAction.java new file mode 100644 index 0000000000..6c0139a908 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/OpenHostsAction.java @@ -0,0 +1,81 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 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.datamodel.hosts; + +import java.awt.Frame; +import java.beans.PropertyChangeEvent; +import java.util.EnumSet; +import javax.swing.Action; +import org.openide.awt.ActionID; +import org.openide.awt.ActionRegistration; +import org.openide.util.HelpCtx; +import org.openide.util.NbBundle; +import org.openide.util.NbBundle.Messages; +import org.openide.util.actions.CallableSystemAction; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.Case; + +/** + * An Action that opens the Host Management window. + */ +@ActionID(category = "Case", id = "org.sleuthkit.autopsy.datamodel.hosts.OpenHostsAction") +@ActionRegistration(displayName = "#CTL_OpenHosts", lazy = false) +@Messages({ + "CTL_OpenHosts=Manage Hosts",}) +public final class OpenHostsAction extends CallableSystemAction { + + private static final long serialVersionUID = 1L; + + /** + * Main constructor. + */ + OpenHostsAction() { + putValue(Action.NAME, Bundle.CTL_OpenHosts()); + this.setEnabled(false); + Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), (PropertyChangeEvent evt) -> { + setEnabled(null != evt.getNewValue()); + }); + } + + @Override + public void performAction() { + Frame parent = WindowManager.getDefault().getMainWindow(); + ManageHostsDialog dialog = new ManageHostsDialog(parent); + dialog.setResizable(false); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + dialog.toFront(); + } + + @Override + @NbBundle.Messages("OpenHostsAction_displayName=Hosts") + public String getName() { + return Bundle.OpenHostsAction_displayName(); + } + + @Override + public HelpCtx getHelpCtx() { + return HelpCtx.DEFAULT_HELP; + } + + @Override + public boolean asynchronous() { + return false; // run on edt + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSProcessor.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSProcessor.java index 3faab8c2cd..7c38dedcf8 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSProcessor.java @@ -159,15 +159,6 @@ public class RawDSProcessor implements DataSourceProcessor, AutoIngestDataSource @Override public void run(Host host, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { configPanel.storeSettings(); - - // HOSTTODO - use passed in value - try { - host = Case.getCurrentCase().getSleuthkitCase().getHostManager().getOrCreateHost("RawDSProcessor Host"); - } catch (TskCoreException ex) { - // It's not worth adding a logger for temporary code - //logger.log(Level.SEVERE, "Error creating/loading host", ex); - host = null; - } run(UUID.randomUUID().toString(), configPanel.getImageFilePath(), configPanel.getTimeZone(), configPanel.getChunkSize(), host, progressMonitor, callback); } diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessor.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessor.java index dfea1da73c..d14d4e542a 100755 --- a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessor.java @@ -218,16 +218,7 @@ public class XRYDataSourceProcessor implements DataSourceProcessor, AutoIngestDa "XRYDataSourceProcessor.noCurrentCase=No case is open." }) public void run(Host host, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { - progressMonitor.setIndeterminate(true); - - // HOSTTODO - use passed in value - try { - host = Case.getCurrentCase().getSleuthkitCase().getHostManager().getOrCreateHost("XRYDSProcessor Host"); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error creating/loading host", ex); - host = null; - } - + progressMonitor.setIndeterminate(true); String selectedFilePath = configPanel.getSelectedFilePath(); File selectedFile = new File(selectedFilePath); Path selectedPath = selectedFile.toPath(); diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactsListPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactsListPanel.java index d1328ef68c..cbf38d7453 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactsListPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactsListPanel.java @@ -162,8 +162,7 @@ final class ArtifactsListPanel extends AbstractArtifactListPanel { @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override void clearList() { - tableModel.setContents(new ArrayList<>()); - tableModel.fireTableDataChanged(); + addArtifacts(new ArrayList<>()); } /** diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainDetailsPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainDetailsPanel.java index cd6e5dbe8e..18ea417423 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainDetailsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainDetailsPanel.java @@ -92,9 +92,9 @@ final class DomainDetailsPanel extends JPanel { if (selectedTabName == null || !selectedTabName.equals(newTabTitle)) { selectedTabName = newTabTitle; Component selectedComponent = jTabbedPane1.getSelectedComponent(); - if (selectedComponent instanceof DomainArtifactsTabPanel) { + if (!StringUtils.isBlank(domain) && selectedComponent instanceof DomainArtifactsTabPanel) { runDomainWorker((DomainArtifactsTabPanel) selectedComponent, true); - } else if (selectedComponent instanceof MiniTimelinePanel) { + } else if (!StringUtils.isBlank(domain) && selectedComponent instanceof MiniTimelinePanel) { runMiniTimelineWorker((MiniTimelinePanel) selectedComponent, true); } } @@ -182,13 +182,13 @@ final class DomainDetailsPanel extends JPanel { @Subscribe void handlePopulateDomainTabsEvent(DiscoveryEventUtils.PopulateDomainTabsEvent populateEvent) { SwingUtilities.invokeLater(() -> { - if (StringUtils.isBlank(populateEvent.getDomain())) { + domain = populateEvent.getDomain(); + if (StringUtils.isBlank(domain)) { resetTabsStatus(); //send fade out event DiscoveryEventUtils.getDiscoveryEventBus().post(new DiscoveryEventUtils.DetailsVisibleEvent(false)); } else { resetTabsStatus(); - domain = populateEvent.getDomain(); Component selectedComponent = jTabbedPane1.getSelectedComponent(); if (selectedComponent instanceof DomainArtifactsTabPanel) { runDomainWorker((DomainArtifactsTabPanel) selectedComponent, false); diff --git a/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestModuleAdapter.java b/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestModuleAdapter.java index 6f79817782..83b5a1457f 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestModuleAdapter.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestModuleAdapter.java @@ -23,6 +23,11 @@ import org.sleuthkit.datamodel.Content; /** * An adapter that provides a no-op implementation of the startUp() method for * data source ingest modules. + * + * NOTE: As of Java 8, interfaces can have default methods. + * DataSourceIngestModule now provides default no-op versions of startUp() and + * shutDown(). This class is no longer needed and can be deprecated when + * convenient. */ public abstract class DataSourceIngestModuleAdapter implements DataSourceIngestModule { diff --git a/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestPipeline.java b/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestPipeline.java index c22d63348c..6c551da602 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestPipeline.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestPipeline.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2011-2016 Basis Technology Corp. + * + * Copyright 2014-2021 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. @@ -18,184 +18,86 @@ */ package org.sleuthkit.autopsy.ingest; -import java.util.ArrayList; -import java.util.Date; import java.util.List; +import java.util.Optional; import java.util.logging.Level; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.Content; /** - * This class manages a sequence of data source level ingest modules for an - * ingestJobPipeline. It starts the modules, runs data sources through them, and - * shuts them down when data source level ingest is complete. - *

- * This class is thread-safe. + * A pipeline of data source level ingest modules for performing data source + * level ingest tasks for an ingest job. */ -final class DataSourceIngestPipeline { +final class DataSourceIngestPipeline extends IngestTaskPipeline { - private static final IngestManager ingestManager = IngestManager.getInstance(); private static final Logger logger = Logger.getLogger(DataSourceIngestPipeline.class.getName()); - private final IngestJobPipeline ingestJobPipeline; - private final List modules = new ArrayList<>(); - private volatile PipelineModule currentModule; + private static final IngestManager ingestManager = IngestManager.getInstance(); /** - * Constructs an object that manages a sequence of data source level ingest - * modules. It starts the modules, runs data sources through them, and shuts - * them down when data source level ingest is complete. + * Constructs a pipeline of data source level ingest modules for performing + * data source level ingest tasks for an ingest job. * - * @param ingestJobPipeline The ingestJobPipeline that owns this pipeline. - * @param moduleTemplates Templates for the creating the ingest modules that - * make up this pipeline. + * @param ingestJobPipeline The ingest job pipeline that owns this pipeline. + * @param moduleTemplates The ingest module templates that define this + * pipeline. */ DataSourceIngestPipeline(IngestJobPipeline ingestJobPipeline, List moduleTemplates) { - this.ingestJobPipeline = ingestJobPipeline; - for (IngestModuleTemplate template : moduleTemplates) { - if (template.isDataSourceIngestModuleTemplate()) { - PipelineModule module = new PipelineModule(template.createDataSourceIngestModule(), template.getModuleName()); - modules.add(module); - } - } + super(ingestJobPipeline, moduleTemplates); } - /** - * Indicates whether or not there are any ingest modules in this pipeline. - * - * @return True or false. - */ - boolean isEmpty() { - return modules.isEmpty(); + @Override + Optional> acceptModuleTemplate(IngestModuleTemplate template) { + Optional> module = Optional.empty(); + if (template.isDataSourceIngestModuleTemplate()) { + DataSourceIngestModule ingestModule = template.createDataSourceIngestModule(); + module = Optional.of(new DataSourcePipelineModule(ingestModule, template.getModuleName())); + } + return module; } - /** - * Starts up the ingest modules in this pipeline. - * - * @return A list of ingest module startup errors, possibly empty. - */ - synchronized List startUp() { - List errors = new ArrayList<>(); - for (PipelineModule module : modules) { - try { - module.startUp(new IngestJobContext(this.ingestJobPipeline)); - } catch (Throwable ex) { // Catch-all exception firewall - errors.add(new IngestModuleError(module.getDisplayName(), ex)); - } - } - return errors; + @Override + void prepareTask(DataSourceIngestTask task) { } - /** - * Runs a data source through the ingest modules in sequential order. - * - * @param task A data source level ingest task containing a data source to - * be processed. - * - * @return A list of processing errors, possible empty. - */ - synchronized List process(DataSourceIngestTask task) { - List errors = new ArrayList<>(); - if (!this.ingestJobPipeline.isCancelled()) { - Content dataSource = task.getDataSource(); - for (PipelineModule module : modules) { - try { - this.currentModule = module; - String displayName = NbBundle.getMessage(this.getClass(), - "IngestJob.progress.dataSourceIngest.displayName", - module.getDisplayName(), dataSource.getName()); - this.ingestJobPipeline.updateDataSourceIngestProgressBarDisplayName(displayName); - this.ingestJobPipeline.switchDataSourceIngestProgressBarToIndeterminate(); - DataSourceIngestPipeline.ingestManager.setIngestTaskProgress(task, module.getDisplayName()); - logger.log(Level.INFO, "{0} analysis of {1} (pipeline={2}) starting", new Object[]{module.getDisplayName(), ingestJobPipeline.getDataSource().getName(), ingestJobPipeline.getId()}); //NON-NLS - module.process(dataSource, new DataSourceIngestModuleProgress(this.ingestJobPipeline)); - logger.log(Level.INFO, "{0} analysis of {1} (pipeline={2}) finished", new Object[]{module.getDisplayName(), ingestJobPipeline.getDataSource().getName(), ingestJobPipeline.getId()}); //NON-NLS - } catch (Throwable ex) { // Catch-all exception firewall - errors.add(new IngestModuleError(module.getDisplayName(), ex)); - } - if (this.ingestJobPipeline.isCancelled()) { - break; - } else if (this.ingestJobPipeline.currentDataSourceIngestModuleIsCancelled()) { - this.ingestJobPipeline.currentDataSourceIngestModuleCancellationCompleted(currentModule.getDisplayName()); - } - } - } - this.currentModule = null; + @Override + void completeTask(DataSourceIngestTask task) { ingestManager.setIngestTaskProgressCompleted(task); - return errors; } /** - * Gets the currently running module. - * - * @return The module, possibly null if no module is currently running. + * A wrapper that adds ingest infrastructure operations to a data source + * level ingest module. */ - PipelineModule getCurrentlyRunningModule() { - return this.currentModule; - } - - /** - * This class decorates a data source level ingest module with a display - * name and a processing start time. - */ - static class PipelineModule implements DataSourceIngestModule { + static final class DataSourcePipelineModule extends IngestTaskPipeline.PipelineModule { private final DataSourceIngestModule module; - private final String displayName; - private volatile Date processingStartTime; /** - * Constructs an object that decorates a data source level ingest module - * with a display name and a processing start time. - * - * @param module The data source level ingest module to be - * decorated. - * @param displayName The display name. + * Constructs a wrapper that adds ingest infrastructure operations to a + * data source level ingest module. */ - PipelineModule(DataSourceIngestModule module, String displayName) { + DataSourcePipelineModule(DataSourceIngestModule module, String displayName) { + super(module, displayName); this.module = module; - this.displayName = displayName; - this.processingStartTime = new Date(); - } - - /** - * Gets the class name of the decorated ingest module. - * - * @return The class name. - */ - String getClassName() { - return this.module.getClass().getCanonicalName(); - } - - /** - * Gets the display of the decorated ingest module. - * - * @return The display name. - */ - String getDisplayName() { - return this.displayName; - } - - /** - * Gets the time the decorated ingest module started processing the data - * source. - * - * @return The start time, will be null if the module has not started - * processing the data source yet. - */ - Date getProcessingStartTime() { - return this.processingStartTime; } @Override - public void startUp(IngestJobContext context) throws IngestModuleException { - this.module.startUp(context); - } - - @Override - public IngestModule.ProcessResult process(Content dataSource, DataSourceIngestModuleProgress statusHelper) { - this.processingStartTime = new Date(); - return this.module.process(dataSource, statusHelper); + void performTask(IngestJobPipeline ingestJobPipeline, DataSourceIngestTask task) throws IngestModuleException { + Content dataSource = task.getDataSource(); + String progressBarDisplayName = NbBundle.getMessage(this.getClass(), "IngestJob.progress.dataSourceIngest.displayName", getDisplayName(), dataSource.getName()); + ingestJobPipeline.updateDataSourceIngestProgressBarDisplayName(progressBarDisplayName); + ingestJobPipeline.switchDataSourceIngestProgressBarToIndeterminate(); + ingestManager.setIngestTaskProgress(task, getDisplayName()); + logger.log(Level.INFO, "{0} analysis of {1} starting", new Object[]{getDisplayName(), dataSource.getName()}); //NON-NLS + ProcessResult result = module.process(dataSource, new DataSourceIngestModuleProgress(ingestJobPipeline)); + logger.log(Level.INFO, "{0} analysis of {1} finished", new Object[]{getDisplayName(), dataSource.getName()}); //NON-NLS + if (!ingestJobPipeline.isCancelled() && ingestJobPipeline.currentDataSourceIngestModuleIsCancelled()) { + ingestJobPipeline.currentDataSourceIngestModuleCancellationCompleted(getDisplayName()); + } + if (result == ProcessResult.ERROR) { + throw new IngestModuleException(String.format("%s experienced an error analyzing %s (data source objId = %d)", getDisplayName(), dataSource.getName(), dataSource.getId())); //NON-NLS + } } } diff --git a/Core/src/org/sleuthkit/autopsy/ingest/FileIngestModule.java b/Core/src/org/sleuthkit/autopsy/ingest/FileIngestModule.java index 215c9e0bcb..60355e10f8 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/FileIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/FileIngestModule.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2014-2021 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,13 +36,4 @@ public interface FileIngestModule extends IngestModule { */ ProcessResult process(AbstractFile file); - /** - * Invoked by Autopsy when an ingest job is completed (either because the - * data has been analyzed or because the job was canceled - check - * IngestJobContext.fileIngestIsCancelled()), before the ingest module - * instance is discarded. The module should respond by doing things like - * releasing private resources, submitting final results, and posting a - * final ingest message. - */ - void shutDown(); } diff --git a/Core/src/org/sleuthkit/autopsy/ingest/FileIngestModuleAdapter.java b/Core/src/org/sleuthkit/autopsy/ingest/FileIngestModuleAdapter.java index 13811e6eb4..9f733308c8 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/FileIngestModuleAdapter.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/FileIngestModuleAdapter.java @@ -23,6 +23,10 @@ import org.sleuthkit.datamodel.AbstractFile; /** * An adapter that provides no-op implementations of the startUp() and * shutDown() methods for file ingest modules. + * + * NOTE: As of Java 8, interfaces can have default methods. FileIngestModule now + * provides default no-op versions of startUp() and shutDown(). This class is no + * longer needed and can be deprecated when convenient. */ public abstract class FileIngestModuleAdapter implements FileIngestModule { diff --git a/Core/src/org/sleuthkit/autopsy/ingest/FileIngestPipeline.java b/Core/src/org/sleuthkit/autopsy/ingest/FileIngestPipeline.java index 2f6604e415..1cc6ce05a4 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/FileIngestPipeline.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/FileIngestPipeline.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2014-2015 Basis Technology Corp. + * + * Copyright 2014-2021 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. @@ -18,223 +18,109 @@ */ package org.sleuthkit.autopsy.ingest; -import java.util.ArrayList; -import java.util.Date; import java.util.List; -import java.util.logging.Level; - -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import java.util.Optional; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.TskCoreException; /** - * This class manages a sequence of file level ingest modules for an - * ingest job pipeline. It starts the modules, runs files through them, and shuts them - * down when file level ingest is complete. - *

- * This class is thread-safe. + * A pipeline of file ingest modules for performing file ingest tasks for an + * ingest job. */ -final class FileIngestPipeline { +final class FileIngestPipeline extends IngestTaskPipeline { private static final IngestManager ingestManager = IngestManager.getInstance(); private final IngestJobPipeline ingestJobPipeline; - private final List modules = new ArrayList<>(); - private Date startTime; - private volatile boolean running; /** - * Constructs an object that manages a sequence of file level ingest - * modules. It starts the modules, runs files through them, and shuts them - * down when file level ingest is complete. + * Constructs a pipeline of file ingest modules for performing file ingest + * tasks for an ingest job. * - * @param ingestJobPipeline The ingestJobPipeline that owns the pipeline. - * @param moduleTemplates The ingest module templates that define the - * pipeline. + * @param ingestJobPipeline The ingest job pipeline that owns this pipeline. + * @param moduleTemplates The ingest module templates that define this + * pipeline. */ FileIngestPipeline(IngestJobPipeline ingestJobPipeline, List moduleTemplates) { + super(ingestJobPipeline, moduleTemplates); this.ingestJobPipeline = ingestJobPipeline; - for (IngestModuleTemplate template : moduleTemplates) { - if (template.isFileIngestModuleTemplate()) { - PipelineModule module = new PipelineModule(template.createFileIngestModule(), template.getModuleName()); - modules.add(module); - } + } + + @Override + Optional> acceptModuleTemplate(IngestModuleTemplate template) { + Optional> module = Optional.empty(); + if (template.isFileIngestModuleTemplate()) { + FileIngestModule ingestModule = template.createFileIngestModule(); + module = Optional.of(new FileIngestPipelineModule(ingestModule, template.getModuleName())); } + return module; } - /** - * Queries whether or not there are any ingest modules in this pipeline. - * - * @return True or false. - */ - boolean isEmpty() { - return this.modules.isEmpty(); + @Override + void prepareTask(FileIngestTask task) throws IngestTaskPipelineException { } - /** - * Queries whether or not this pipeline is running. - * - * @return True or false. - */ - boolean isRunning() { - return this.running; - } - - /** - * Returns the start up time of this pipeline. - * - * @return The file processing start time, may be null if this pipeline has - * not been started yet. - */ - Date getStartTime() { - return this.startTime; - } - - /** - * Starts up all of the ingest modules in the pipeline. - * - * @return List of start up errors, possibly empty. - */ - synchronized List startUp() { - this.startTime = new Date(); - this.running = true; - List errors = new ArrayList<>(); - for (PipelineModule module : this.modules) { - try { - module.startUp(new IngestJobContext(this.ingestJobPipeline)); - } catch (Throwable ex) { // Catch-all exception firewall - errors.add(new IngestModuleError(module.getDisplayName(), ex)); - } + @Override + void completeTask(FileIngestTask task) throws IngestTaskPipelineException { + AbstractFile file = null; + try { + file = task.getFile(); + } catch (TskCoreException ex) { + throw new IngestTaskPipelineException(String.format("Failed to get file (file objId = %d)", task.getFileId()), ex); //NON-NLS } - return errors; - } - - /** - * Runs a file through the ingest modules in sequential order. - * - * @param task A file level ingest task containing a file to be processed. - * - * @return A list of processing errors, possible empty. - */ - synchronized List process(FileIngestTask task) { - List errors = new ArrayList<>(); - if (!this.ingestJobPipeline.isCancelled()) { - AbstractFile file; - try { - file = task.getFile(); - } catch (TskCoreException ex) { - // In practice, this task would never have been enqueued since the file - // lookup would have failed there. - errors.add(new IngestModuleError("File Ingest Pipeline", ex)); // NON-NLS - FileIngestPipeline.ingestManager.setIngestTaskProgressCompleted(task); - return errors; + try { + if (!ingestJobPipeline.isCancelled()) { + /* + * Save any updates from the ingest modules to the case + * database. + */ + file.save(); } - for (PipelineModule module : this.modules) { - try { - FileIngestPipeline.ingestManager.setIngestTaskProgress(task, module.getDisplayName()); - this.ingestJobPipeline.setCurrentFileIngestModule(module.getDisplayName(), task.getFile().getName()); - module.process(file); - } catch (Throwable ex) { // Catch-all exception firewall - errors.add(new IngestModuleError(module.getDisplayName(), ex)); - } - if (this.ingestJobPipeline.isCancelled()) { - break; - } - } - - if (!this.ingestJobPipeline.isCancelled()) { - // Save any properties that have not already been saved to the database - try{ - file.save(); - } catch (TskCoreException ex){ - Logger.getLogger(FileIngestPipeline.class.getName()).log(Level.SEVERE, "Failed to save data for file " + file.getId(), ex); //NON-NLS - } + } catch (TskCoreException ex) { + throw new IngestTaskPipelineException(String.format("Failed to save updated data for file (file objId = %d)", task.getFileId()), ex); //NON-NLS + } finally { + if (!ingestJobPipeline.isCancelled()) { IngestManager.getInstance().fireFileIngestDone(file); } file.close(); + ingestManager.setIngestTaskProgressCompleted(task); } - FileIngestPipeline.ingestManager.setIngestTaskProgressCompleted(task); - return errors; } /** - * Shuts down all of the modules in the pipeline. - * - * @return A list of shut down errors, possibly empty. + * A wrapper that adds ingest infrastructure operations to a file ingest + * module. */ - synchronized List shutDown() { - List errors = new ArrayList<>(); - if (this.running == true) { // Don't shut down pipelines that never started - for (PipelineModule module : this.modules) { - try { - module.shutDown(); - } catch (Throwable ex) { // Catch-all exception firewall - errors.add(new IngestModuleError(module.getDisplayName(), ex)); - String msg = ex.getMessage(); - // Jython run-time errors don't seem to have a message, but have details in toString. - if (msg == null) { - msg = ex.toString(); - } - MessageNotifyUtil.Notify.error(NbBundle.getMessage(this.getClass(), "FileIngestPipeline.moduleError.title.text", module.getDisplayName()), msg); - } - } - } - this.running = false; - return errors; - } - - /** - * This class decorates a file level ingest module with a display name. - */ - private static final class PipelineModule implements FileIngestModule { + static final class FileIngestPipelineModule extends IngestTaskPipeline.PipelineModule { private final FileIngestModule module; - private final String displayName; /** - * Constructs an object that decorates a file level ingest module with a - * display name. + * Constructs a wrapper that adds ingest infrastructure operations to a + * file ingest module. * - * @param module The file level ingest module to be decorated. - * @param displayName The display name. + * + * @param module The module. + * @param displayName The display name of the module. */ - PipelineModule(FileIngestModule module, String displayName) { + FileIngestPipelineModule(FileIngestModule module, String displayName) { + super(module, displayName); this.module = module; - this.displayName = displayName; - } - - /** - * Gets the class name of the decorated ingest module. - * - * @return The class name. - */ - String getClassName() { - return module.getClass().getCanonicalName(); - } - - /** - * Gets the display name of the decorated ingest module. - * - * @return The display name. - */ - String getDisplayName() { - return displayName; } @Override - public void startUp(IngestJobContext context) throws IngestModuleException { - module.startUp(context); - } - - @Override - public IngestModule.ProcessResult process(AbstractFile file) { - return module.process(file); - } - - @Override - public void shutDown() { - module.shutDown(); + void performTask(IngestJobPipeline ingestJobPipeline, FileIngestTask task) throws IngestModuleException { + AbstractFile file = null; + try { + file = task.getFile(); + } catch (TskCoreException ex) { + throw new IngestModuleException(String.format("Failed to get file (file objId = %d)", task.getFileId()), ex); //NON-NLS + } + ingestManager.setIngestTaskProgress(task, getDisplayName()); + ingestJobPipeline.setCurrentFileIngestModule(getDisplayName(), file.getName()); + ProcessResult result = module.process(file); + if (result == ProcessResult.ERROR) { + throw new IngestModuleException(String.format("%s experienced an error analyzing %s (file objId = %d)", getDisplayName(), file.getName(), file.getId())); //NON-NLS + } } } diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestJob.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestJob.java index c73dd940d0..decccd4e84 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestJob.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestJob.java @@ -416,7 +416,7 @@ public final class IngestJob { Snapshot snapshot = pipeline.getSnapshot(getIngestTasksSnapshot); dataSourceProcessingSnapshots.add(new DataSourceProcessingSnapshot(snapshot)); if (null == dataSourceModule) { - DataSourceIngestPipeline.PipelineModule module = snapshot.getDataSourceLevelIngestModule(); + DataSourceIngestPipeline.DataSourcePipelineModule module = snapshot.getDataSourceLevelIngestModule(); if (null != module) { dataSourceModule = new DataSourceIngestModuleHandle(ingestJobPipelines.get(snapshot.getJobId()), module); } @@ -500,7 +500,7 @@ public final class IngestJob { public static class DataSourceIngestModuleHandle { private final IngestJobPipeline ingestJobPipeline; - private final DataSourceIngestPipeline.PipelineModule module; + private final DataSourceIngestPipeline.DataSourcePipelineModule module; private final boolean cancelled; /** @@ -511,7 +511,7 @@ public final class IngestJob { * @param ingestJobPipeline The ingestJobPipeline that owns the data source level ingest module. * @param module The data source level ingest module. */ - private DataSourceIngestModuleHandle(IngestJobPipeline ingestJobPipeline, DataSourceIngestPipeline.PipelineModule module) { + private DataSourceIngestModuleHandle(IngestJobPipeline ingestJobPipeline, DataSourceIngestPipeline.DataSourcePipelineModule module) { this.ingestJobPipeline = ingestJobPipeline; this.module = module; this.cancelled = ingestJobPipeline.currentDataSourceIngestModuleIsCancelled(); diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestJobPipeline.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestJobPipeline.java index d5ba4c783b..384f5c63c5 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestJobPipeline.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestJobPipeline.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014-2019 Basis Technology Corp. + * Copyright 2014-2021 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -553,7 +553,7 @@ final class IngestJobPipeline { /* * If the data-source-level ingest pipelines were successfully started, - * start the Start the file-level ingest pipelines (one per file ingest + * start the file-level ingest pipelines (one per pipeline file ingest * thread). */ if (errors.isEmpty()) { @@ -940,7 +940,7 @@ final class IngestJobPipeline { synchronized (this.dataSourceIngestPipelineLock) { if (!this.isCancelled() && !this.currentDataSourceIngestPipeline.isEmpty()) { List errors = new ArrayList<>(); - errors.addAll(this.currentDataSourceIngestPipeline.process(task)); + errors.addAll(this.currentDataSourceIngestPipeline.performTask(task)); if (!errors.isEmpty()) { logIngestModuleErrors(errors); } @@ -1014,7 +1014,7 @@ final class IngestJobPipeline { * Run the file through the pipeline. */ List errors = new ArrayList<>(); - errors.addAll(pipeline.process(task)); + errors.addAll(pipeline.performTask(task)); if (!errors.isEmpty()) { logIngestModuleErrors(errors, file); } @@ -1232,9 +1232,9 @@ final class IngestJobPipeline { * * @return The currently running module, may be null. */ - DataSourceIngestPipeline.PipelineModule getCurrentDataSourceIngestModule() { - if (null != this.currentDataSourceIngestPipeline) { - return this.currentDataSourceIngestPipeline.getCurrentlyRunningModule(); + DataSourceIngestPipeline.DataSourcePipelineModule getCurrentDataSourceIngestModule() { + if (null != currentDataSourceIngestPipeline) { + return (DataSourceIngestPipeline.DataSourcePipelineModule) currentDataSourceIngestPipeline.getCurrentlyRunningModule(); } else { return null; } @@ -1274,7 +1274,7 @@ final class IngestJobPipeline { } } } - + // If a data source had no tasks in progress it may now be complete. checkForStageCompleted(); } @@ -1353,18 +1353,18 @@ final class IngestJobPipeline { logErrorMessage(Level.SEVERE, String.format("%s experienced an error during analysis", error.getModuleDisplayName()), error.getThrowable()); //NON-NLS } } - + /** * Write ingest module errors to the log. * * @param errors The errors. - * @param file AbstractFile that caused the errors. + * @param file AbstractFile that caused the errors. */ private void logIngestModuleErrors(List errors, AbstractFile file) { for (IngestModuleError error : errors) { logErrorMessage(Level.SEVERE, String.format("%s experienced an error during analysis while processing file %s, object ID %d", error.getModuleDisplayName(), file.getName(), file.getId()), error.getThrowable()); //NON-NLS } - } + } /** * Gets a snapshot of this jobs state and performance. diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java index 95696378b7..791a45021c 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012-2019 Basis Technology Corp. + * Copyright 2012-2021 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestModule.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestModule.java index c5628d1912..8bfeef57fb 100755 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestModule.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2014-2021 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,9 +25,8 @@ package org.sleuthkit.autopsy.ingest; * ingest job it performs (one for each thread that it is using). * * Autopsy will call startUp() before any data is processed, will pass any data - * to be analyzed into the process() method (FileIngestModule.process() or - * DataSourceIngestModule.process()), and call shutDown() after either all data - * is analyzed or the user has canceled the job. + * to be analyzed into the process() method, and call shutDown() after either + * all data is analyzed or the user has canceled the job. * * Autopsy may use multiple threads to complete an ingest job, but it is * guaranteed that a module instance will always be called from a single thread. @@ -47,25 +46,53 @@ package org.sleuthkit.autopsy.ingest; public interface IngestModule { /** - * A return code for derived class process() methods. + * Invoked by Autopsy to allow an ingest module instance to set up any + * internal data structures and acquire any private resources it will need + * during an ingest job. If the module depends on loading any resources, it + * should do so in this method so that it can throw an exception in the case + * of an error and alert the user. Exceptions that are thrown from startUp() + * are logged and stop processing of the data source. + * + * IMPORTANT: If the module instances must share resources, the modules are + * responsible for synchronizing access to the shared resources and doing + * reference counting as required to release those resources correctly. + * Also, more than one ingest job may be in progress at any given time. This + * must also be taken into consideration when sharing resources between + * module instances. See IngestModuleReferenceCounter. + * + * @param context Provides data and services specific to the ingest job and + * the ingest pipeline of which the module is a part. + * + * @throws org.sleuthkit.autopsy.ingest.IngestModule.IngestModuleException */ - public enum ProcessResult { - - OK, - ERROR - }; + default void startUp(IngestJobContext context) throws IngestModuleException { + } /** - * A custom exception for the use of ingest modules. + * Invoked by Autopsy when an ingest job is completed (either because the + * data has been analyzed or because the job was cancelled), before the + * ingest module instance is discarded. The module should respond by doing + * things like releasing private resources, submitting final results, and + * posting a final ingest message. + * + * IMPORTANT: If the module instances must share resources, the modules are + * responsible for synchronizing access to the shared resources and doing + * reference counting as required to release those resources correctly. + * Also, more than one ingest job may be in progress at any given time. This + * must also be taken into consideration when sharing resources between + * module instances. See IngestModuleReferenceCounter. + * + */ + default void shutDown() { + } + + /** + * An exception for the use of ingest modules. */ public class IngestModuleException extends Exception { private static final long serialVersionUID = 1L; - @Deprecated - public IngestModuleException() { - } - public IngestModuleException(String message) { super(message); } @@ -73,26 +100,21 @@ public interface IngestModule { public IngestModuleException(String message, Throwable cause) { super(message, cause); } + + @Deprecated + public IngestModuleException() { + } + } /** - * Invoked by Autopsy to allow an ingest module instance to set up any - * internal data structures and acquire any private resources it will need - * during an ingest job. If the module depends on loading any resources, it - * should do so in this method so that it can throw an exception in the case - * of an error and alert the user. Exceptions that are thrown from process() - * and shutDown() are logged, but do not stop processing of the data source. - * - * @param context Provides data and services specific to the ingest job and - * the ingest pipeline of which the module is a part. - * - * @throws org.sleuthkit.autopsy.ingest.IngestModule.IngestModuleException + * A return code for subclass process() methods. */ - void startUp(IngestJobContext context) throws IngestModuleException; + public enum ProcessResult { + + OK, + ERROR + + }; - /** - * TODO: The next time an API change is legal, add a cancel() method and - * remove the "ingest job is canceled" queries from the IngestJobContext - * class. - */ } diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestTaskPipeline.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestTaskPipeline.java new file mode 100755 index 0000000000..5e918d4ddb --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestTaskPipeline.java @@ -0,0 +1,345 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 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.ingest; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; + +/** + * An abstract superclass for pipelines of ingest modules for a given ingest + * task type. Some examples of ingest task types: data source level ingest + * tasks, file ingest tasks, data artifact ingest tasks, etc. Subclasses need to + * implement a specialization of the inner PipelineModule abstract superclass + * for the type of ingest modules that make up the pipeline. + * + * @param The ingest task type. + */ +abstract class IngestTaskPipeline { + + private static final IngestManager ingestManager = IngestManager.getInstance(); + private final IngestJobPipeline ingestJobPipeline; + private final List moduleTemplates; + private final List> modules; + private volatile Date startTime; + private volatile boolean running; + private volatile PipelineModule currentModule; + + /** + * Constructs an instance of an abstract superclass for pipelines of ingest + * modules for a given ingest task type. Some examples of ingest task types: + * data source level ingest tasks, file ingest tasks, data artifact ingest + * tasks, etc. Subclasses need to implement a specialization of the inner + * PipelineModule abstract superclass for the type of ingest modules that + * make up the pipeline. + * + * @param ingestJobPipeline The ingest job pipeline that owns this pipeline. + * @param moduleTemplates The ingest module templates that define this + * pipeline. + */ + IngestTaskPipeline(IngestJobPipeline ingestJobPipeline, List moduleTemplates) { + this.ingestJobPipeline = ingestJobPipeline; + this.moduleTemplates = moduleTemplates; + modules = new ArrayList<>(); + } + + /** + * Indicates whether or not there are any ingest modules in this pipeline. + * + * @return True or false. + */ + boolean isEmpty() { + return modules.isEmpty(); + } + + /** + * Queries whether or not this pipeline is running, i.e., started and not + * shut down. + * + * @return True or false. + */ + boolean isRunning() { + return running; + } + + /** + * Starts up the ingest modules in this pipeline. + * + * @return A list of ingest module startup errors, possibly empty. + */ + List startUp() { + createIngestModules(moduleTemplates); + return startUpIngestModules(); + } + + /** + * Creates the ingest modules for this pipeline. + * + * @param moduleTemplates The ingest module templates avaialble to this + * pipeline. + */ + private void createIngestModules(List moduleTemplates) { + for (IngestModuleTemplate template : moduleTemplates) { + Optional> module = acceptModuleTemplate(template); + if (module.isPresent()) { + modules.add(module.get()); + } + } + } + + /** + * Determines if the type of ingest module that can be created from a given + * ingest module template should be added to this pipeline. If so, the + * ingest module is created and returned. + * + * @param ingestModuleTemplate The ingest module template to be used or + * ignored, as appropriate to the pipeline type. + * + * @return An Optional that is either empty or contains a newly created and + * wrapped ingest module. + */ + abstract Optional> acceptModuleTemplate(IngestModuleTemplate ingestModuleTemplate); + + /** + * Starts up the ingest modules in the pipeline. + * + * @return A list of ingest module startup errors, possibly empty. + */ + private List startUpIngestModules() { + startTime = new Date(); + running = true; + List errors = new ArrayList<>(); + for (PipelineModule module : modules) { + try { + module.startUp(new IngestJobContext(ingestJobPipeline)); + } catch (Throwable ex) { // Catch-all exception firewall + errors.add(new IngestModuleError(module.getDisplayName(), ex)); + } + } + return errors; + } + + /** + * Returns the start up time of this pipeline. + * + * @return The file processing start time, may be null if this pipeline has + * not been started yet. + */ + Date getStartTime() { + if (startTime == null) { + throw new IllegalStateException("startUp() was not called"); //NON-NLS + } + return new Date(startTime.getTime()); + } + + /** + * Does any preparation required before performing a task. + * + * @param task The task. + * + * @throws IngestTaskPipelineException Thrown if there is an error preparing + * to perform the task. + */ + abstract void prepareTask(T task) throws IngestTaskPipelineException; + + /** + * Performs an ingest task using the ingest modules in this pipeline. + * + * @param task The task. + * + * @return A list of ingest module task processing errors, possibly empty. + */ + List performTask(T task) { + List errors = new ArrayList<>(); + if (!this.ingestJobPipeline.isCancelled()) { + try { + prepareTask(task); + } catch (IngestTaskPipelineException ex) { + errors.add(new IngestModuleError("Ingest Task Pipeline", ex)); //NON-NLS + return errors; + } + for (PipelineModule module : modules) { + try { + currentModule = module; + currentModule.setProcessingStartTime(); + module.performTask(ingestJobPipeline, task); + } catch (Throwable ex) { // Catch-all exception firewall + errors.add(new IngestModuleError(module.getDisplayName(), ex)); + } + if (ingestJobPipeline.isCancelled()) { + break; + } + } + } + try { + completeTask(task); + } catch (IngestTaskPipelineException ex) { + errors.add(new IngestModuleError("Ingest Task Pipeline", ex)); //NON-NLS + } + currentModule = null; + return errors; + } + + /** + * Gets the currently running module. + * + * @return The module, possibly null if no module is currently running. + */ + PipelineModule getCurrentlyRunningModule() { + return currentModule; + } + + /** + * Does any clean up required after performing a task. + * + * @param task The task. + * + * @throws IngestTaskPipelineException Thrown if there is an error cleaning + * up after performing the task. + */ + abstract void completeTask(T task) throws IngestTaskPipelineException; + + /** + * Shuts down all of the modules in the pipeline. + * + * @return A list of shut down errors, possibly empty. + */ + List shutDown() { + List errors = new ArrayList<>(); + if (running == true) { + for (PipelineModule module : modules) { + try { + module.shutDown(); + } catch (Throwable ex) { // Catch-all exception firewall + errors.add(new IngestModuleError(module.getDisplayName(), ex)); + String msg = ex.getMessage(); + if (msg == null) { + /* + * Jython run-time errors don't seem to have a message, + * but have details in the string returned by + * toString(). + */ + msg = ex.toString(); + } + MessageNotifyUtil.Notify.error(NbBundle.getMessage(this.getClass(), "FileIngestPipeline.moduleError.title.text", module.getDisplayName()), msg); + } + } + } + running = false; + return errors; + } + + /** + * An abstract superclass for a wrapper that adds ingest infrastructure + * operations to an ingest module. + */ + static abstract class PipelineModule implements IngestModule { + + private final IngestModule module; + private final String displayName; + private volatile Date processingStartTime; + + /** + * Constructs an instance of an abstract superclass for a wrapper that + * adds ingest infrastructure operations to an ingest module. + * + * @param module The ingest module to be wrapped. + * @param displayName The display name for the module. + */ + PipelineModule(IngestModule module, String displayName) { + this.module = module; + this.displayName = displayName; + this.processingStartTime = new Date(); + } + + /** + * Gets the class name of the wrapped ingest module. + * + * @return The class name. + */ + String getClassName() { + return module.getClass().getCanonicalName(); + } + + /** + * Gets the display name of the wrapped ingest module. + * + * @return The display name. + */ + String getDisplayName() { + return displayName; + } + + /** + * Sets the processing start time for the wrapped module to the system + * time when this method is called. + */ + void setProcessingStartTime() { + processingStartTime = new Date(); + } + + /** + * Gets the the processing start time for the wrapped module. + * + * @return The start time, will be null if the module has not started + * processing the data source yet. + */ + Date getProcessingStartTime() { + return new Date(processingStartTime.getTime()); + } + + @Override + public void startUp(IngestJobContext context) throws IngestModuleException { + module.startUp(context); + } + + /** + * Performs an ingest task. + * + * @param ingestJobPipeline The ingest job pipeline that owns the ingest + * module pipeline this module belongs to. + * @param task The task to process. + * + * @throws IngestModuleException Excepton thrown if there is an error + * performing the task. + */ + abstract void performTask(IngestJobPipeline ingestJobPipeline, T task) throws IngestModuleException; + + } + + /** + * An exception for the use of ingest task pipelines. + */ + public static class IngestTaskPipelineException extends Exception { + + private static final long serialVersionUID = 1L; + + public IngestTaskPipelineException(String message) { + super(message); + } + + public IngestTaskPipelineException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/ingest/Snapshot.java b/Core/src/org/sleuthkit/autopsy/ingest/Snapshot.java index 19a0e41c35..dc7eceaad8 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/Snapshot.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/Snapshot.java @@ -34,7 +34,7 @@ public final class Snapshot implements Serializable { private final long jobId; private final long jobStartTime; private final long snapShotTime; - transient private final DataSourceIngestPipeline.PipelineModule dataSourceLevelIngestModule; + transient private final DataSourceIngestPipeline.DataSourcePipelineModule dataSourceLevelIngestModule; private final boolean fileIngestRunning; private final Date fileIngestStartTime; private final long processedFiles; @@ -48,7 +48,7 @@ public final class Snapshot implements Serializable { * Constructs an object to store basic diagnostic statistics for a data * source ingest job. */ - Snapshot(String dataSourceName, long jobId, long jobStartTime, DataSourceIngestPipeline.PipelineModule dataSourceIngestModule, + Snapshot(String dataSourceName, long jobId, long jobStartTime, DataSourceIngestPipeline.DataSourcePipelineModule dataSourceIngestModule, boolean fileIngestRunning, Date fileIngestStartTime, boolean jobCancelled, IngestJob.CancellationReason cancellationReason, List cancelledModules, long processedFiles, long estimatedFilesToProcess, @@ -110,7 +110,7 @@ public final class Snapshot implements Serializable { return jobStartTime; } - DataSourceIngestPipeline.PipelineModule getDataSourceLevelIngestModule() { + DataSourceIngestPipeline.DataSourcePipelineModule getDataSourceLevelIngestModule() { return this.dataSourceLevelIngestModule; } diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/LogicalImagerDSProcessor.java b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/LogicalImagerDSProcessor.java index 67498064f0..9d9054cb80 100644 --- a/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/LogicalImagerDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/LogicalImagerDSProcessor.java @@ -160,17 +160,7 @@ public final class LogicalImagerDSProcessor implements DataSourceProcessor { }) @Override public void run(Host host, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { - configPanel.storeSettings(); - - // HOSTTODO - set to value from config panel - try { - host = Case.getCurrentCase().getSleuthkitCase().getHostManager().getOrCreateHost("LogicalImagerDSProcessor Host"); - } catch (TskCoreException ex) { - // It's not worth adding a logger for temporary code - //logger.log(Level.SEVERE, "Error creating/loading host", ex); - host = null; - } - + configPanel.storeSettings(); Path imageDirPath = configPanel.getImageDirPath(); List errorList = new ArrayList<>(); List emptyDataSources = new ArrayList<>(); diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSProcessor.java b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSProcessor.java index a0ca322fac..ea8a45fda9 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSProcessor.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSProcessor.java @@ -140,15 +140,6 @@ public class MemoryDSProcessor implements DataSourceProcessor { @Override public void run(Host host, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { configPanel.storeSettings(); - - // HOSTTODO - replace with a call to configPanel().getHost() - try { - host = Case.getCurrentCase().getSleuthkitCase().getHostManager().getOrCreateHost("MemoryDSProcessor Host"); - } catch (TskCoreException ex) { - // It's not worth adding a logger for temporary code - //logger.log(Level.SEVERE, "Error creating/loading host", ex); - host = null; - } run(UUID.randomUUID().toString(), configPanel.getImageFilePath(), configPanel.getProfile(), configPanel.getPluginsToRun(), configPanel.getTimeZone(), host, progressMonitor, callback); } diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java index e230bfda5a..1b1661c82e 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java @@ -67,7 +67,9 @@ import java.util.Scanner; import java.util.Set; import java.util.HashSet; import static java.util.Locale.US; +import java.util.Optional; import static java.util.TimeZone.getTimeZone; +import java.util.stream.Collectors; import org.openide.util.Lookup; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; @@ -93,10 +95,19 @@ import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAM import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_USER_ID; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_USER_NAME; +import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_HOME_DIR; import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.Host; +import org.sleuthkit.datamodel.HostManager; +import org.sleuthkit.datamodel.OsAccount; +import org.sleuthkit.datamodel.OsAccountAttribute; +import org.sleuthkit.datamodel.OsAccountManager; +import org.sleuthkit.datamodel.OsAccountRealm; import org.sleuthkit.datamodel.ReadContentInputStream.ReadContentInputStreamException; import org.sleuthkit.datamodel.Report; import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskDataException; /** * Extract windows registry data using regripper. Runs two versions of @@ -172,10 +183,16 @@ class ExtractRegistry extends Extract { private static final String SHELLBAG_ARTIFACT_NAME = "RA_SHELL_BAG"; //NON-NLS private static final String SHELLBAG_ATTRIBUTE_LAST_WRITE = "RA_SHELL_BAG_LAST_WRITE"; //NON-NLS private static final String SHELLBAG_ATTRIBUTE_KEY = "RA_SHELL_BAG_KEY"; //NON-NLS - + + private static final SimpleDateFormat REG_RIPPER_TIME_FORMAT = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy 'Z'", US); + BlackboardArtifact.Type shellBagArtifactType = null; BlackboardAttribute.Type shellBagKeyAttributeType = null; BlackboardAttribute.Type shellBagLastWriteAttributeType = null; + + static { + REG_RIPPER_TIME_FORMAT.setTimeZone(getTimeZone("GMT")); + } ExtractRegistry() throws IngestModuleException { moduleName = NbBundle.getMessage(ExtractIE.class, "ExtractRegistry.moduleName.text"); @@ -354,7 +371,7 @@ class ExtractRegistry extends Extract { if (regFileNameLocal.toLowerCase().contains("sam") && parseSamPluginOutput(regOutputFiles.fullPlugins, regFile) == false) { this.addErrorMessage( NbBundle.getMessage(this.getClass(), "ExtractRegistry.analyzeRegFiles.failedParsingResults", - this.getName(), regFileName)); + this.getName(), regFileName)); } else if (regFileNameLocal.toLowerCase().contains("ntuser") || regFileNameLocal.toLowerCase().contains("usrclass")) { try { List shellbags = ShellBagParser.parseShellbagOutput(regOutputFiles.fullPlugins); @@ -849,56 +866,66 @@ class ExtractRegistry extends Extract { case "ProfileList": //NON-NLS try { - String homeDir = value; - String sid = artnode.getAttribute("sid"); //NON-NLS - String username = artnode.getAttribute("username"); //NON-NLS - BlackboardArtifact bbart = null; - try { - //check if any of the existing artifacts match this username - ArrayList existingArtifacts = currentCase.getSleuthkitCase().getBlackboardArtifacts(ARTIFACT_TYPE.TSK_OS_ACCOUNT); - for (BlackboardArtifact artifact : existingArtifacts) { - if (artifact.getDataSource().getId() == regFile.getDataSourceObjectId()) { - BlackboardAttribute attribute = artifact.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_USER_ID)); - if (attribute != null && attribute.getValueString().equals(sid)) { - bbart = artifact; - break; + String homeDir = value; + String sid = artnode.getAttribute("sid"); //NON-NLS + String username = artnode.getAttribute("username"); //NON-NLS + + // For now both an OsAccount and the + // TSK_OS_ACCOUNT artifact will be created. + try{ + createOrUpdateOsAccount(regFile, sid, username, homeDir); + + } catch(TskCoreException | TskDataException ex) { + logger.log(Level.SEVERE, String.format("Failed to create OsAccount for file: %s, sid: %s", regFile.getId(), sid)); + } + + BlackboardArtifact bbart = null; + try { + //check if any of the existing artifacts match this username + ArrayList existingArtifacts = currentCase.getSleuthkitCase().getBlackboardArtifacts(ARTIFACT_TYPE.TSK_OS_ACCOUNT); + for (BlackboardArtifact artifact : existingArtifacts) { + if (artifact.getDataSource().getId() == regFile.getDataSourceObjectId()) { + BlackboardAttribute attribute = artifact.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_USER_ID)); + if (attribute != null && attribute.getValueString().equals(sid)) { + bbart = artifact; + break; + } } } + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error getting existing os account artifact", ex); } - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting existing os account artifact", ex); - } - if (bbart == null) { - //create new artifact - bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_OS_ACCOUNT); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_USER_NAME, - parentModuleName, username)); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_USER_ID, - parentModuleName, sid)); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PATH, - parentModuleName, homeDir)); - - newArtifacts.add(bbart); - } else { - //add attributes to existing artifact - BlackboardAttribute bbattr = bbart.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_USER_NAME)); - - if (bbattr == null) { + if (bbart == null) { + //create new artifact + bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_OS_ACCOUNT); bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_USER_NAME, parentModuleName, username)); - } - bbattr = bbart.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_PATH)); - if (bbattr == null) { + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_USER_ID, + parentModuleName, sid)); bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PATH, parentModuleName, homeDir)); - } - } - bbart.addAttributes(bbattributes); + + newArtifacts.add(bbart); + } else { + //add attributes to existing artifact + BlackboardAttribute bbattr = bbart.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_USER_NAME)); + + if (bbattr == null) { + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_USER_NAME, + parentModuleName, username)); + } + bbattr = bbart.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_PATH)); + if (bbattr == null) { + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PATH, + parentModuleName, homeDir)); + } + } + bbart.addAttributes(bbattributes); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error adding account artifact to blackboard.", ex); //NON-NLS - } - break; + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error adding account artifact to blackboard.", ex); //NON-NLS + } + break; case "NtuserNetwork": // NON-NLS try { @@ -1116,6 +1143,34 @@ class ExtractRegistry extends Extract { for (Map userInfo : userSet) { userInfoMap.put(userInfo.get(SID_KEY), userInfo); } + + // New OsAccount Code + OsAccountManager accountMgr = tskCase.getOsAccountManager(); + HostManager hostMrg = tskCase.getHostManager(); + Host host = hostMrg.getHost((DataSource)dataSource); + + List existingAccounts = accountMgr.getAccounts(host); + for(OsAccount osAccount: existingAccounts) { + Optional optional = osAccount.getUniqueIdWithinRealm(); + if(!optional.isPresent()) { + continue; + } + + String sid = optional.get(); + Map userInfo = userInfoMap.remove(sid); + if(userInfo != null) { + updateOsAccount(osAccount, userInfo, groupMap.get(sid), regAbstractFile); + } + } + + //add remaining userinfos as accounts; + for (Map userInfo : userInfoMap.values()) { + OsAccount osAccount = accountMgr.createWindowsAccount(userInfo.get(SID_KEY), null, null, host, OsAccountRealm.RealmScope.UNKNOWN); + updateOsAccount(osAccount, userInfo, groupMap.get(userInfo.get(SID_KEY)), regAbstractFile); + } + + // Existing TSK_OS_ACCOUNT code. + //get all existing OS account artifacts List existingOsAccounts = tskCase.getBlackboardArtifacts(ARTIFACT_TYPE.TSK_OS_ACCOUNT); for (BlackboardArtifact osAccount : existingOsAccounts) { @@ -1157,7 +1212,7 @@ class ExtractRegistry extends Extract { logger.log(Level.WARNING, "Error building the document parser: {0}", ex); //NON-NLS } catch (ParseException ex) { logger.log(Level.WARNING, "Error parsing the the date from the registry file", ex); //NON-NLS - } catch (TskCoreException ex) { + } catch (TskDataException | TskCoreException ex) { logger.log(Level.WARNING, "Error updating TSK_OS_ACCOUNT artifacts to include newly parsed data.", ex); //NON-NLS } finally { if (!context.dataSourceIngestIsCancelled()) { @@ -1166,7 +1221,7 @@ class ExtractRegistry extends Extract { } return false; } - + /** * Creates the attribute list for the given user information and group list. * @@ -2138,4 +2193,281 @@ class ExtractRegistry extends Extract { public String autopsyPlugins = ""; public String fullPlugins = ""; } + + /** + * Updates an existing or creates a new OsAccount with the given attributes. + * + * @param file Registry file + * @param sid Account sid + * @param userName Login name + * @param homeDir Account home Directory + * + * @throws TskCoreException + * @throws TskDataException + */ + private void createOrUpdateOsAccount(AbstractFile file, String sid, String userName, String homeDir) throws TskCoreException, TskDataException { + OsAccountManager accountMgr = tskCase.getOsAccountManager(); + HostManager hostMrg = tskCase.getHostManager(); + Host host = hostMrg.getHost((DataSource)dataSource); + + Optional optional = accountMgr.getWindowsAccount(sid, null, null, host); + OsAccount osAccount; + if (!optional.isPresent()) { + osAccount = accountMgr.createWindowsAccount(sid, userName != null && userName.isEmpty() ? null : userName, null, host, OsAccountRealm.RealmScope.UNKNOWN); + } else { + osAccount = optional.get(); + if (userName != null && !userName.isEmpty()) { + osAccount.setLoginName(userName); + } + } + + if (homeDir != null && !homeDir.isEmpty()) { + List attributes = new ArrayList<>(); + attributes.add(createOsAccountAttribute(TSK_HOME_DIR, homeDir, osAccount, host, file)); + osAccount.addAttributes(attributes); + } + + accountMgr.updateAccount(osAccount); + } + + /** + * Create an account for the found email address. + * + * @param regFile File the account was found in + * @param emailAddress The emailAddress + */ + private void addEmailAccount(AbstractFile regFile, String emailAddress) { + try { + currentCase.getSleuthkitCase() + .getCommunicationsManager() + .createAccountFileInstance(Account.Type.EMAIL, + emailAddress, getRAModuleName(), regFile); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, + String.format("Error adding email account with value " + + "%s, to the case database for file %s [objId=%d]", + emailAddress, regFile.getName(), regFile.getId()), ex); + } + } + + /** + * Parse the data time string found in the SAM file. + * + * @param value Date time string in the REG_RIPPER_TIME_FORMAT + * + * @return Java epoch time in seconds or null if the value could not be + * parsed. + */ + private Long parseRegRipTime(String value) { + try { + return REG_RIPPER_TIME_FORMAT.parse(value).getTime() / MS_IN_SEC; + } catch (ParseException ex) { + logger.log(Level.SEVERE, String.format("Failed to parse reg rip time: %s", value)); + } + return null; + } + + /** + * Parse the data from the userInfo map created by parsing the SAM file. + * + * @param osAccount Account to update. + * @param userInfo userInfo map from SAM file parsing. + * @param groupList Group list from the SAM file parsing. + * @param regFile Source file. + * + * @throws TskDataException + * @throws TskCoreException + */ + private void updateOsAccount(OsAccount osAccount, Map userInfo, List groupList, AbstractFile regFile) throws TskDataException, TskCoreException { + Host host = ((DataSource)dataSource).getHost(); + + SimpleDateFormat regRipperTimeFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy 'Z'", US); + regRipperTimeFormat.setTimeZone(getTimeZone("GMT")); + + List attributes = new ArrayList<>(); + + String value = userInfo.get(ACCOUNT_CREATED_KEY); + if (value != null && !value.isEmpty() && !value.equals(NEVER_DATE)) { + Long time = parseRegRipTime(value); + if (time != null) { + osAccount.setCreationTime(time); + } + } + + value = userInfo.get(LAST_LOGIN_KEY); + if (value != null && !value.isEmpty() && !value.equals(NEVER_DATE)) { + Long time = parseRegRipTime(value); + if (time != null) { + attributes.add(createOsAccountAttribute(TSK_DATETIME_ACCESSED, + parseRegRipTime(value), + osAccount, host, regFile)); + } + } + + value = userInfo.get(LOGIN_COUNT_KEY); + if (value != null && !value.isEmpty()) { + attributes.add(createOsAccountAttribute(ATTRIBUTE_TYPE.TSK_COUNT, + Integer.parseInt(value), + osAccount, host, regFile)); + } + + // From regripper the possible values for this key are + // "Default Admin User", "Custom Limited Acct" + // and "Default Guest Acct" + value = userInfo.get(ACCOUNT_TYPE_KEY); + if (value != null && !value.isEmpty()) { + osAccount.setIsAdmin(value.toLowerCase().contains("Admin")); + } + + value = userInfo.get(USER_COMMENT_KEY); + if (value != null && !value.isEmpty()) { + attributes.add(createOsAccountAttribute(ATTRIBUTE_TYPE.TSK_DESCRIPTION, + value, osAccount, host, regFile)); + } + + value = userInfo.get(INTERNET_NAME_KEY); + if (value != null && !value.isEmpty()) { + addEmailAccount(regFile, value); + + attributes.add(createOsAccountAttribute(ATTRIBUTE_TYPE.TSK_EMAIL, + value, osAccount, host, regFile)); + } + + // FULL_NAME_KEY and NAME_KEY appear to be the same value. + value = userInfo.get(FULL_NAME_KEY); + if (value != null && !value.isEmpty()) { + osAccount.setFullName(value); + } else { + value = userInfo.get(NAME_KEY); + if (value != null && !value.isEmpty()) { + osAccount.setFullName(value); + } + } + + value = userInfo.get(PWD_RESET_KEY); + if (value != null && !value.isEmpty() && !value.equals(NEVER_DATE)) { + Long time = parseRegRipTime(value); + if (time != null) { + attributes.add(createOsAccountAttribute(ATTRIBUTE_TYPE.TSK_DATETIME_PASSWORD_RESET, + time, osAccount, host, regFile)); + } + } + + value = userInfo.get(PASSWORD_HINT); + if (value != null && !value.isEmpty()) { + attributes.add(createOsAccountAttribute(ATTRIBUTE_TYPE.TSK_PASSWORD_HINT, + value, osAccount, host, regFile)); + } + + value = userInfo.get(PWD_FAILE_KEY); + if (value != null && !value.isEmpty() && !value.equals(NEVER_DATE)) { + Long time = parseRegRipTime(value); + if (time != null) { + attributes.add(createOsAccountAttribute(ATTRIBUTE_TYPE.TSK_DATETIME_PASSWORD_FAIL, + time, osAccount, host, regFile)); + } + } + + String settingString = getSettingsFromMap(ACCOUNT_SETTINGS_FLAGS, userInfo); + if (!settingString.isEmpty()) { + settingString = settingString.substring(0, settingString.length() - 2); + attributes.add(createOsAccountAttribute(ATTRIBUTE_TYPE.TSK_PASSWORD_SETTINGS, + settingString, osAccount, host, regFile)); + } + + settingString = getSettingsFromMap(ACCOUNT_SETTINGS_FLAGS, userInfo); + if (!settingString.isEmpty()) { + settingString = settingString.substring(0, settingString.length() - 2); + attributes.add(createOsAccountAttribute(ATTRIBUTE_TYPE.TSK_ACCOUNT_SETTINGS, + settingString, osAccount, host, regFile)); + } + + settingString = getSettingsFromMap(ACCOUNT_TYPE_FLAGS, userInfo); + if (!settingString.isEmpty()) { + attributes.add(createOsAccountAttribute(ATTRIBUTE_TYPE.TSK_FLAG, + settingString, osAccount, host, regFile)); + } + + if (groupList != null && groupList.isEmpty()) { + String groups = groupList.stream() + .map(String::valueOf) + .collect(Collectors.joining(", ")); + + attributes.add(createOsAccountAttribute(ATTRIBUTE_TYPE.TSK_GROUPS, + groups, osAccount, host, regFile)); + } + + osAccount.addAttributes(attributes); + tskCase.getOsAccountManager().updateAccount(osAccount); + } + + /** + * Create comma separated list from the set values for the given keys. + * + * @param keys List of map keys. + * @param map Data map. + * + * @return Comma separated String of values. + */ + private String getSettingsFromMap(String[] keys, Map map) { + List settingsList = new ArrayList<>(); + for (String setting : keys) { + if (map.containsKey(setting)) { + settingsList.add(setting); + } + } + + if (!settingsList.isEmpty()) { + return settingsList.stream() + .map(String::valueOf) + .collect(Collectors.joining(", ")); + } + + return ""; + } + + /** + * Helper for constructing a new OsAccountAttribute + * + * @param type Attribute type + * @param value The value to store + * @param osAccount The OsAccount this attribute belongs to + * @param host The Host related to the OsAccount + * @param file The source where the attribute was found. + * + * @return Newly created OsACcountAttribute + */ + private OsAccountAttribute createOsAccountAttribute(BlackboardAttribute.ATTRIBUTE_TYPE type, String value, OsAccount osAccount, Host host, AbstractFile file) { + return new OsAccountAttribute(new BlackboardAttribute.Type(type), value, osAccount, host, file); + } + + /** + * Helper for constructing a new OsAccountAttribute + * + * @param type Attribute type + * @param value The value to store + * @param osAccount The OsAccount this attribute belongs to + * @param host The Host related to the OsAccount + * @param file The source where the attribute was found. + * + * @return Newly created OsACcountAttribute + */ + private OsAccountAttribute createOsAccountAttribute(BlackboardAttribute.ATTRIBUTE_TYPE type, Long value, OsAccount osAccount, Host host, AbstractFile file) { + return new OsAccountAttribute(new BlackboardAttribute.Type(type), value, osAccount, host, file); + } + + /** + * Helper for constructing a new OsAccountAttribute + * + * @param type Attribute type + * @param value The value to store + * @param osAccount The OsAccount this attribute belongs to + * @param host The Host related to the OsAccount + * @param file The source where the attribute was found. + * + * @return Newly created OsACcountAttribute + */ + private OsAccountAttribute createOsAccountAttribute(BlackboardAttribute.ATTRIBUTE_TYPE type, Integer value, OsAccount osAccount, Host host, AbstractFile file) { + return new OsAccountAttribute(new BlackboardAttribute.Type(type), value, osAccount, host, file); + } } diff --git a/Testing/src/org/sleuthkit/autopsy/testing/AutopsyTestCases.java b/Testing/src/org/sleuthkit/autopsy/testing/AutopsyTestCases.java index de8cf07fc8..c4d5c8dae8 100644 --- a/Testing/src/org/sleuthkit/autopsy/testing/AutopsyTestCases.java +++ b/Testing/src/org/sleuthkit/autopsy/testing/AutopsyTestCases.java @@ -143,6 +143,10 @@ public class AutopsyTestCases { while (!wo.btNext().isEnabled()) { new Timeout("pausing", 1000).sleep(); // give it a second till the Add Data Source dialog enabled } + + // pass by host menu with auto-generate host (which should already be selected) + wo.btNext().clickMouse(); + //select the toggle button for Disk Image or VM File it will be the first button created and proceed to next panel JToggleButtonOperator jtbo = new JToggleButtonOperator(wo, 0); jtbo.clickMouse(); @@ -173,6 +177,10 @@ public class AutopsyTestCases { while (!wo.btNext().isEnabled()) { new Timeout("pausing", 1000).sleep(); // give it a second till the Add Data Source dialog enabled } + + // pass by host menu with auto-generate host (which should already be selected) + wo.btNext().clickMouse(); + //select the toggle button for Logical Files it will be the third button created and proceed to next panel JToggleButtonOperator jtbo = new JToggleButtonOperator(wo, 2); jtbo.clickMouse(); diff --git a/test/script/regression.py b/test/script/regression.py index e217ef8694..1b42f623bc 100644 --- a/test/script/regression.py +++ b/test/script/regression.py @@ -348,9 +348,10 @@ class TestRunner(object): mu_su_str = "multi" if test_data.isMultiUser else "single" for file in glob.glob(test_data.output_path + "/*-Diff.txt"): + newPath = test_data.main_config.args.diff_files_output_folder + "/" + test_data.image + "-" + mu_su_str + "-" + os.path.basename(file) # Eg. copies HTML-Report-Diff.txt to -HTML-Report-Diff.txt - shutil.copy(file, test_data.main_config.args.diff_files_output_folder + - "/" + test_data.image + "-" + mus_su_str + "-" + os.path.basename(file)) + print('copying ' + str(file) + ' to ' + newPath) + shutil.copy(file, newPath) copied = True if not copied: print_report([], "NO DIFF FILES COPIED FROM " + test_data.output_path, "")