diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/Bundle.properties b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/Bundle.properties index cca8104c38..bf6adc0de3 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/Bundle.properties @@ -23,10 +23,13 @@ FileTypeIdGlobalSettingsPanel.mimeTypeLabel.text=MIME Type FileTypeIdGlobalSettingsPanel.saveTypeButton.text=Save FileTypeIdGlobalSettingsPanel.signatureComboBox.rawItem=Bytes (Hex) FileTypeIdGlobalSettingsPanel.signatureComboBox.asciiItem=String (ASCII) +FileTypeIdGlobalSettingsPanel.offsetComboBox.startItem=Start +FileTypeIdGlobalSettingsPanel.offsetComboBox.endItem=End FileTypeIdGlobalSettingsPanel.JOptionPane.invalidMIMEType.message=MIME type is required. FileTypeIdGlobalSettingsPanel.JOptionPane.invalidMIMEType.title=Missing MIME Type FileTypeIdGlobalSettingsPanel.JOptionPane.invalidSignature.message=Signature is required. FileTypeIdGlobalSettingsPanel.JOptionPane.invalidSignature.title=Missing Signature +FileTypeIdGlobalSettingsPanel.JOptionPane.invalidOffset.length=Offset must not be smaller than signature size. FileTypeIdGlobalSettingsPanel.JOptionPane.invalidOffset.message=Offset must be a positive integer. FileTypeIdGlobalSettingsPanel.JOptionPane.invalidOffset.title=Invalid Offset FileTypeIdGlobalSettingsPanel.JOptionPane.invalidRawSignatureBytes.message=The signature has one or more invalid hexadecimal digits. @@ -46,4 +49,4 @@ FileTypeIdGlobalSettingsPanel.jLabel2.text=MIME Types: FileTypeIdGlobalSettingsPanel.jLabel3.text=Autopsy can automatically detect many file types. Add your custom file types here. FileTypeIdGlobalSettingsPanel.startUp.fileTypeDetectorInitializationException.msg=Error initializing the file type detector. FileTypeIdIngestModule.startUp.fileTypeDetectorInitializationException.msg=Error initializing the file type detector. - +FileTypeIdGlobalSettingsPanel.offsetRelativeToLabel.text=Offset is relative to diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileType.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileType.java index 30c9525603..84e4186975 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileType.java +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileType.java @@ -51,7 +51,7 @@ class FileType { */ FileType(String mimeType, final Signature signature, String filesSetName, boolean alert) { this.mimeType = mimeType; - this.signature = new Signature(signature.getSignatureBytes(), signature.getOffset(), signature.getType()); + this.signature = new Signature(signature.getSignatureBytes(), signature.getOffset(), signature.getType(), signature.isRelativeToStart()); this.interestingFilesSetName = filesSetName; this.alert = alert; } @@ -71,7 +71,7 @@ class FileType { * @return The signature. */ Signature getSignature() { - return new Signature(signature.getSignatureBytes(), signature.getOffset(), signature.getType()); + return new Signature(signature.getSignatureBytes(), signature.getOffset(), signature.getType(), signature.isRelativeToStart()); } /** @@ -148,6 +148,7 @@ class FileType { private final byte[] signatureBytes; private final long offset; private final Type type; + private final boolean isRelativeToStart; /** * Creates a file signature consisting of a sequence of bytes at a @@ -162,6 +163,7 @@ class FileType { this.signatureBytes = Arrays.copyOf(signatureBytes, signatureBytes.length); this.offset = offset; this.type = type; + this.isRelativeToStart = true; } /** @@ -175,6 +177,7 @@ class FileType { this.signatureBytes = signatureString.getBytes(StandardCharsets.US_ASCII); this.offset = offset; this.type = Type.ASCII; + this.isRelativeToStart = true; } /** @@ -190,6 +193,56 @@ class FileType { this.signatureBytes = Arrays.copyOf(signatureBytes, signatureBytes.length); this.offset = offset; this.type = Type.RAW; + this.isRelativeToStart = true; + } + + /** + * Creates a file signature consisting of a sequence of bytes at a + * specific offset within a file. + * + * @param signatureBytes The signature bytes. + * @param offset The offset of the signature bytes. + * @param type The type of data in the byte array. Impacts + * how it is displayed to the user in the UI. + * @param isRelativeToStart Determines whether this signature is relative to start. + */ + Signature(final byte[] signatureBytes, long offset, Type type, boolean isRelativeToStart) { + this.signatureBytes = Arrays.copyOf(signatureBytes, signatureBytes.length); + this.offset = offset; + this.type = type; + this.isRelativeToStart = isRelativeToStart; + } + + /** + * Creates a file signature consisting of an ASCII string at a + * specific offset within a file. + * + * @param signatureString The ASCII string + * @param offset The offset of the signature bytes. + * @param isRelativeToStart Determines whether this signature is relative to start. + */ + Signature(String signatureString, long offset, boolean isRelativeToStart) { + this.signatureBytes = signatureString.getBytes(StandardCharsets.US_ASCII); + this.offset = offset; + this.type = Type.ASCII; + this.isRelativeToStart = isRelativeToStart; + } + + /** + * Creates a file signature consisting of a sequence of bytes at a + * specific offset within a file. If bytes correspond to an ASCII + * string, use one of the other constructors so that the string is + * displayed to the user instead of the raw bytes. + * + * @param signatureBytes The signature bytes. + * @param offset The offset of the signature bytes. + * @param isRelativeToStart Determines whether this signature is relative to start. + */ + Signature(final byte[] signatureBytes, long offset, boolean isRelativeToStart) { + this.signatureBytes = Arrays.copyOf(signatureBytes, signatureBytes.length); + this.offset = offset; + this.type = Type.RAW; + this.isRelativeToStart = isRelativeToStart; } /** @@ -218,6 +271,10 @@ class FileType { Type getType() { return type; } + + boolean isRelativeToStart() { + return isRelativeToStart; + } /** * Determines whether or not the signature is contained within a given @@ -228,12 +285,15 @@ class FileType { * @return True or false. */ boolean containedIn(final AbstractFile file) { - if (file.getSize() < (offset + signatureBytes.length)) { + long actualOffset = offset; + if(!isRelativeToStart) + actualOffset = file.getSize() - 1 - offset; + if (file.getSize() < (actualOffset + signatureBytes.length)) { return false; /// too small, can't contain this signature } try { byte[] buffer = new byte[signatureBytes.length]; - int bytesRead = file.read(buffer, offset, signatureBytes.length); + int bytesRead = file.read(buffer, actualOffset, signatureBytes.length); return ((bytesRead == signatureBytes.length) && (Arrays.equals(buffer, signatureBytes))); } catch (TskCoreException ex) { /** diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdGlobalSettingsPanel.form b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdGlobalSettingsPanel.form index bfc83ee709..2b4647356c 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdGlobalSettingsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdGlobalSettingsPanel.form @@ -49,40 +49,50 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -107,7 +117,7 @@ - + @@ -138,12 +148,17 @@ + + + + + - - + + - + @@ -350,5 +365,22 @@ + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdGlobalSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdGlobalSettingsPanel.java index 5380234490..9e1b1f3320 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdGlobalSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdGlobalSettingsPanel.java @@ -52,6 +52,8 @@ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPane private static final String RAW_SIGNATURE_TYPE_COMBO_BOX_ITEM = NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.signatureComboBox.rawItem"); private static final String ASCII_SIGNATURE_TYPE_COMBO_BOX_ITEM = NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.signatureComboBox.asciiItem"); + private static final String START_OFFSET_RELATIVE_COMBO_BOX_ITEM = NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.offsetComboBox.startItem"); + private static final String END_OFFSET_RELATIVE_COMBO_BOX_ITEM = NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.offsetComboBox.endItem"); /** * The list model for the file types list component of this panel is the set * of MIME types associated with the user-defined file types. A mapping of @@ -88,6 +90,7 @@ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPane private void customizeComponents() { setFileTypesListModel(); setSignatureTypeComboBoxModel(); + setOffsetRealtiveToComboBoxModel(); clearTypeDetailsComponents(); addTypeListSelectionListener(); addTextFieldListeners(); @@ -111,6 +114,17 @@ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPane signatureTypeComboBox.setModel(sigTypeComboBoxModel); signatureTypeComboBox.setSelectedItem(FileTypeIdGlobalSettingsPanel.RAW_SIGNATURE_TYPE_COMBO_BOX_ITEM); } + + /** + * Sets the model for the signature type combo box. + */ + private void setOffsetRealtiveToComboBoxModel() { + DefaultComboBoxModel offsetRelComboBoxModel = new DefaultComboBoxModel<>(); + offsetRelComboBoxModel.addElement(FileTypeIdGlobalSettingsPanel.START_OFFSET_RELATIVE_COMBO_BOX_ITEM); + offsetRelComboBoxModel.addElement(FileTypeIdGlobalSettingsPanel.END_OFFSET_RELATIVE_COMBO_BOX_ITEM); + offsetRelativeToComboBox.setModel(offsetRelComboBoxModel); + offsetRelativeToComboBox.setSelectedItem(FileTypeIdGlobalSettingsPanel.START_OFFSET_RELATIVE_COMBO_BOX_ITEM); + } /** * Adds a listener to the types list component so that the components in the @@ -270,6 +284,7 @@ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPane } } signatureTextField.setText(signatureBytes); + offsetRelativeToComboBox.setSelectedItem(signature.isRelativeToStart() ? FileTypeIdGlobalSettingsPanel.START_OFFSET_RELATIVE_COMBO_BOX_ITEM : FileTypeIdGlobalSettingsPanel.END_OFFSET_RELATIVE_COMBO_BOX_ITEM); offsetTextField.setText(Long.toString(signature.getOffset())); postHitCheckBox.setSelected(fileType.alertOnMatch()); filesSetNameTextField.setEnabled(postHitCheckBox.isSelected()); @@ -289,6 +304,7 @@ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPane signatureTypeComboBox.setSelectedItem(FileTypeIdGlobalSettingsPanel.RAW_SIGNATURE_TYPE_COMBO_BOX_ITEM); hexPrefixLabel.setVisible(true); signatureTextField.setText("0000"); //NON-NLS + offsetRelativeToComboBox.setSelectedItem(FileTypeIdGlobalSettingsPanel.START_OFFSET_RELATIVE_COMBO_BOX_ITEM); offsetTextField.setText(""); //NON-NLS postHitCheckBox.setSelected(false); filesSetNameTextField.setText(""); //NON-NLS @@ -360,6 +376,8 @@ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPane jLabel1 = new javax.swing.JLabel(); jLabel2 = new javax.swing.JLabel(); jLabel3 = new javax.swing.JLabel(); + offsetRelativeToComboBox = new javax.swing.JComboBox(); + offsetRelativeToLabel = new javax.swing.JLabel(); setMaximumSize(new java.awt.Dimension(500, 300)); setPreferredSize(new java.awt.Dimension(500, 300)); @@ -439,6 +457,8 @@ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPane org.openide.awt.Mnemonics.setLocalizedText(jLabel3, org.openide.util.NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.jLabel3.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(offsetRelativeToLabel, org.openide.util.NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.offsetRelativeToLabel.text")); // NOI18N + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( @@ -465,33 +485,40 @@ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPane .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addComponent(mimeTypeLabel) - .addGap(30, 30, 30) - .addComponent(mimeTypeTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 176, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addComponent(postHitCheckBox) - .addGroup(layout.createSequentialGroup() - .addComponent(signatureTypeLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(signatureTypeComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 176, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGroup(layout.createSequentialGroup() - .addComponent(signatureLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 73, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(hexPrefixLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(signatureTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 160, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGroup(layout.createSequentialGroup() - .addComponent(offsetLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 71, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(offsetTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 178, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() .addGap(21, 21, 21) .addComponent(filesSetNameLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(filesSetNameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 182, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addComponent(filesSetNameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 182, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(mimeTypeLabel) + .addGap(30, 30, 30) + .addComponent(mimeTypeTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 176, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(postHitCheckBox) + .addGroup(layout.createSequentialGroup() + .addComponent(signatureTypeLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(signatureTypeComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 176, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createSequentialGroup() + .addComponent(signatureLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 73, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(hexPrefixLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(signatureTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 160, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createSequentialGroup() + .addComponent(offsetLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 71, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(offsetTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 178, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGap(6, 6, 6))) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() .addComponent(saveTypeButton) - .addGap(8, 8, 8)))) + .addGap(8, 8, 8)) + .addGroup(layout.createSequentialGroup() + .addComponent(offsetRelativeToLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(offsetRelativeToComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))) .addComponent(jLabel1) .addComponent(jLabel3)) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))) @@ -509,7 +536,7 @@ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPane .addGroup(layout.createSequentialGroup() .addComponent(jLabel2) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(typesScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 177, Short.MAX_VALUE) + .addComponent(typesScrollPane) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(deleteTypeButton) @@ -532,11 +559,15 @@ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPane .addComponent(hexPrefixLabel) .addComponent(signatureTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addComponent(signatureLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(offsetLabel) + .addComponent(offsetTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(offsetTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(offsetLabel)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(offsetRelativeToComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(offsetRelativeToLabel)) + .addGap(16, 16, 16) .addComponent(postHitCheckBox) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) @@ -596,6 +627,7 @@ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPane byte[] signatureBytes; if (FileType.Signature.Type.RAW == sigType) { try { + sigString = sigString.replaceAll("\\s", ""); signatureBytes = DatatypeConverter.parseHexBinary(sigString); } catch (IllegalArgumentException ex) { JOptionPane.showMessageDialog(null, @@ -612,8 +644,16 @@ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPane * Get the offset. */ long offset; + boolean isRelativeToStart = offsetRelativeToComboBox.getSelectedItem() == FileTypeIdGlobalSettingsPanel.START_OFFSET_RELATIVE_COMBO_BOX_ITEM; try { - offset = Long.parseUnsignedLong(offsetTextField.getText()); + offset = Long.parseUnsignedLong(offsetTextField.getText()); + if(!isRelativeToStart && signatureBytes.length > offset+1) { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.JOptionPane.invalidOffset.length"), + NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.JOptionPane.invalidOffset.title"), + JOptionPane.ERROR_MESSAGE); + return; + } } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(null, NbBundle.getMessage(FileTypeIdGlobalSettingsPanel.class, "FileTypeIdGlobalSettingsPanel.JOptionPane.invalidOffset.message"), @@ -640,7 +680,7 @@ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPane /** * Put it all together and reset the file types list component. */ - FileType.Signature signature = new FileType.Signature(signatureBytes, offset, sigType); + FileType.Signature signature = new FileType.Signature(signatureBytes, offset, sigType, isRelativeToStart); FileType fileType = new FileType(typeName, signature, filesSetName, postHitCheckBox.isSelected()); FileType selected = typesList.getSelectedValue(); if (selected != null) { @@ -683,6 +723,8 @@ final class FileTypeIdGlobalSettingsPanel extends IngestModuleGlobalSettingsPane private javax.swing.JTextField mimeTypeTextField; private javax.swing.JButton newTypeButton; private javax.swing.JLabel offsetLabel; + private javax.swing.JComboBox offsetRelativeToComboBox; + private javax.swing.JLabel offsetRelativeToLabel; private javax.swing.JTextField offsetTextField; private javax.swing.JCheckBox postHitCheckBox; private javax.swing.JButton saveTypeButton; diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypes.xsd b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypes.xsd index 26aa720ff1..d5812bcf19 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypes.xsd +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypes.xsd @@ -13,8 +13,16 @@ - - + + + + + + + + + + @@ -25,7 +33,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/UserDefinedFileTypesManager.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/UserDefinedFileTypesManager.java index d06750dc66..d8a7275282 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/UserDefinedFileTypesManager.java +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/UserDefinedFileTypesManager.java @@ -38,6 +38,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.coreutils.XMLUtil; import org.sleuthkit.autopsy.modules.filetypeid.FileType.Signature; +import org.w3c.dom.Node; import org.xml.sax.SAXException; /** @@ -67,6 +68,7 @@ final class UserDefinedFileTypesManager { private static final String SIGNATURE_TYPE_ATTRIBUTE = "type"; //NON-NLS private static final String BYTES_TAG_NAME = "Bytes"; //NON-NLS private static final String OFFSET_TAG_NAME = "Offset"; //NON-NLS + private static final String RELATIVE_ATTRIBUTE = "RelativeToStart"; private static final String INTERESTING_FILES_SET_TAG_NAME = "InterestingFileSset"; //NON-NLS private static final String ALERT_ATTRIBUTE = "alert"; //NON-NLS private static final String ENCODING_FOR_XML_FILE = "UTF-8"; //NON-NLS @@ -376,6 +378,7 @@ final class UserDefinedFileTypesManager { Element offsetElem = doc.createElement(OFFSET_TAG_NAME); offsetElem.setTextContent(DatatypeConverter.printLong(signature.getOffset())); + offsetElem.setAttribute(RELATIVE_ATTRIBUTE, String.valueOf(signature.isRelativeToStart())); signatureElem.appendChild(offsetElem); signatureElem.setAttribute(SIGNATURE_TYPE_ATTRIBUTE, signature.getType().toString()); @@ -485,10 +488,17 @@ final class UserDefinedFileTypesManager { String sigBytesString = getChildElementTextContent(signatureElem, BYTES_TAG_NAME); byte[] signatureBytes = DatatypeConverter.parseHexBinary(sigBytesString); - String offsetString = getChildElementTextContent(signatureElem, OFFSET_TAG_NAME); + Element offsetElem = (Element) signatureElem.getElementsByTagName(OFFSET_TAG_NAME).item(0); + String offsetString = offsetElem.getTextContent(); long offset = DatatypeConverter.parseLong(offsetString); + + String relativeString = offsetElem.getAttribute(RELATIVE_ATTRIBUTE); + if(relativeString == null || relativeString.equals("")) + return new Signature(signatureBytes, offset, signatureType); + + boolean isRelative = DatatypeConverter.parseBoolean(relativeString); - return new Signature(signatureBytes, offset, signatureType); + return new Signature(signatureBytes, offset, signatureType, isRelative); } /** @@ -526,11 +536,14 @@ final class UserDefinedFileTypesManager { * @param elem The parent element. * @param tagName The tag name of the child element. * - * @return The text content. + * @return The text content or null if the tag doesn't exist. */ private static String getChildElementTextContent(Element elem, String tagName) { NodeList childElems = elem.getElementsByTagName(tagName); - Element childElem = (Element) childElems.item(0); + Node childNode = childElems.item(0); + if(childNode == null) + return null; + Element childElem = (Element) childNode; return childElem.getTextContent(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/Back.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/Back.java index af77a6231d..1313b18483 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/Back.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/Back.java @@ -32,13 +32,16 @@ import org.sleuthkit.autopsy.timeline.TimeLineController; */ //TODO: This and the corresponding imageanalyzer action are identical except for the type of the controller... abstract something! -jm public class Back extends Action { - + private static final Image BACK_IMAGE = new Image("/org/sleuthkit/autopsy/timeline/images/arrow-180.png", 16, 16, true, true, true); // NON-NLS private final TimeLineController controller; - + + @NbBundle.Messages({"Back.text=Back", + "Back.longText=Go back to the last view settings."}) public Back(TimeLineController controller) { - super(NbBundle.getMessage(Back.class, "Back.actions.name.text")); + super(Bundle.Back_text()); + setLongText(Bundle.Back_longText()); setGraphic(new ImageView(BACK_IMAGE)); setAccelerator(new KeyCodeCombination(KeyCode.LEFT, KeyCodeCombination.ALT_DOWN)); this.controller = controller; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/actions/Bundle.properties deleted file mode 100644 index 1140e0d19b..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/Bundle.properties +++ /dev/null @@ -1,4 +0,0 @@ -Back.actions.name.text=Back -DefaultFilters.action.name.text=apply default filters -Forward.action.name.text=Forward - diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/Forward.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/Forward.java index 41df360a3a..b0b7e3e29b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/Forward.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/Forward.java @@ -37,8 +37,9 @@ public class Forward extends Action { private final TimeLineController controller; + @NbBundle.Messages("Forward.text=Forward") public Forward(TimeLineController controller) { - super(NbBundle.getMessage(Forward.class, "Forward.action.name.text")); + super(Bundle.Forward_text()); setGraphic(new ImageView(BACK_IMAGE)); setAccelerator(new KeyCodeCombination(KeyCode.RIGHT, KeyCodeCombination.ALT_DOWN)); this.controller = controller; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/ResetFilters.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/ResetFilters.java index c869c871c8..3d38fb0e4e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/ResetFilters.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/ResetFilters.java @@ -32,8 +32,11 @@ public class ResetFilters extends Action { private FilteredEventsModel eventsModel; + @NbBundle.Messages({"ResetFilters.text=Reset all filters", + "RestFilters.longText=Reset all filters to their default state."}) public ResetFilters(final TimeLineController controller) { - super(NbBundle.getMessage(ResetFilters.class, "DefaultFilters.action.name.text")); + super(Bundle.ResetFilters_text()); + setLongText(Bundle.RestFilters_longText()); eventsModel = controller.getEventsModel(); disabledProperty().bind(new BooleanBinding() { { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java index e3a06617c7..ec80b5180d 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2014-15 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,48 +18,63 @@ */ package org.sleuthkit.autopsy.timeline.actions; +import java.awt.Desktop; import java.io.File; -import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.Writer; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; +import java.time.LocalDateTime; import java.util.function.Consumer; import java.util.logging.Level; import javafx.embed.swing.SwingFXUtils; import javafx.event.ActionEvent; import javafx.scene.Node; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.scene.image.WritableImage; -import javafx.stage.DirectoryChooser; -import javafx.util.Pair; +import javafx.stage.Modality; +import javafx.stage.StageStyle; import javax.imageio.ImageIO; +import javax.swing.JOptionPane; +import org.controlsfx.control.HyperlinkLabel; import org.controlsfx.control.action.Action; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.FileUtil; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; import org.sleuthkit.datamodel.TskCoreException; /** + * Save a snapshot of the given node as an autopsy report. */ public class SaveSnapshotAsReport extends Action { - private static final Image SNAP_SHOT = new Image("org/sleuthkit/autopsy/timeline/images/image.png", 16, 16, true, true); - private static final String HTML_EXT = ".html"; - private static final String REPORT_IMAGE_EXTENSION = ".png"; - private static final Logger LOGGER = Logger.getLogger(SaveSnapshotAsReport.class.getName()); + private static final Image SNAP_SHOT = new Image("org/sleuthkit/autopsy/timeline/images/image.png", 16, 16, true, true); // NON-NLS + private static final String HTML_EXT = ".html"; // NON-NLS + private static final String REPORT_IMAGE_EXTENSION = ".png"; // NON-NLS + private static final ButtonType open = new ButtonType(Bundle.OpenReportAction_DisplayName(), ButtonBar.ButtonData.NO); + private static final ButtonType ok = new ButtonType(ButtonType.OK.getText(), ButtonBar.ButtonData.CANCEL_CLOSE); - @NbBundle.Messages({"SaveSnapshot.action.name.text=Snapshot", + @NbBundle.Messages({"SaveSnapshot.action.name.text=Snapshot Report", "SaveSnapshot.action.longText=Save a screen capture of the visualization as a report.", - "SaveSnapshot.fileChoose.title.text=Save snapshot to",}) + "SaveSnapshot.fileChoose.title.text=Save snapshot to", + "# {0} - report file path", + "SaveSnapShotAsReport.ReportSavedAt=Report saved at [{0}]", + "Timeline.ModuleName=Timeline", "SaveSnapShotAsReport.Success=Success", + "# {0} - uniqueness identifier, local date time at report creation time", + "SaveSnapsHotAsReport.ReportName=timeline-report-{0}", + "SaveSnapShotAsReport.FailedToAddReport=Failed to add snaphot as a report. See log for details", + "# {0} - report name", + "SaveSnapShotAsReport.ErrorWritingReport=Error writing report {0} to disk. See log for details",}) public SaveSnapshotAsReport(TimeLineController controller, Node node) { super(Bundle.SaveSnapshot_action_name_text()); setLongText(Bundle.SaveSnapshot_action_longText()); @@ -67,78 +82,132 @@ public class SaveSnapshotAsReport extends Action { setEventHandler(new Consumer() { @Override - public void accept(ActionEvent t) { - //choose location/name - DirectoryChooser fileChooser = new DirectoryChooser(); - fileChooser.setTitle(Bundle.SaveSnapshot_fileChoose_title_text()); - fileChooser.setInitialDirectory(new File(Case.getCurrentCase().getReportDirectory())); - File reportDirectory = fileChooser.showDialog(null); - if (reportDirectory == null) { - return; - } - reportDirectory.mkdir(); - String reportName = reportDirectory.getName(); - String reportPath = reportDirectory.getPath(); - - //gather metadata - List> reportMetaData = new ArrayList<>(); - - reportMetaData.add(new Pair<>("Case", Case.getCurrentCase().getName())); // NON-NLS + public void accept(ActionEvent actioneEvent) { + String escapedLocalDateTime = FileUtil.escapeFileName(LocalDateTime.now().toString()); + String reportName = Bundle.SaveSnapsHotAsReport_ReportName(escapedLocalDateTime); + Path reportPath = Paths.get(Case.getCurrentCase().getReportDirectory(), reportName).toAbsolutePath(); + File reportHTMLFIle = reportPath.resolve(reportName + HTML_EXT).toFile(); ZoomParams zoomParams = controller.getEventsModel().zoomParametersProperty().get(); - reportMetaData.add(new Pair<>("Time Range", zoomParams.getTimeRange().toString())); // NON-NLS - reportMetaData.add(new Pair<>("Description Level of Detail", zoomParams.getDescriptionLOD().getDisplayName())); // NON-NLS - reportMetaData.add(new Pair<>("Event Type Zoom Level", zoomParams.getTypeZoomLevel().getDisplayName())); // NON-NLS - reportMetaData.add(new Pair<>("Filters", zoomParams.getFilter().getHTMLReportString())); // NON-NLS - //save snapshot as png try { - WritableImage snapshot = node.snapshot(null, null); - ImageIO.write(SwingFXUtils.fromFXImage(snapshot, null), "png", - new File(reportPath, reportName + REPORT_IMAGE_EXTENSION)); // NON-NLS - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "failed to write snapshot to disk", ex); // NON-NLS - return; - } + //ensure directory exists and write html file + Files.createDirectories(reportPath); + try (Writer htmlWriter = new FileWriter(reportHTMLFIle)) { + writeHTMLFile(reportName, htmlWriter, zoomParams); + } - //build html string - StringBuilder wrapper = new StringBuilder(); - wrapper.append("\n\n\t").append("timeline snapshot").append("\n\t\n\n\n"); // NON-NLS - wrapper.append("
\n

").append(reportDirectory.getName()).append("

\n"); // NON-NLS - wrapper.append("\"snaphot\""); // NON-NLS - wrapper.append("\n"); // NON-NLS - for (Pair pair : reportMetaData) { - wrapper.append("\n"); // NON-NLS - } - wrapper.append("
").append(pair.getKey()).append(": ").append(pair.getValue()).append("
\n"); // NON-NLS - wrapper.append("
\n\n"); // NON-NLS - File reportHTMLFIle = new File(reportDirectory, reportName + HTML_EXT); + //take snapshot and save in report directory + ImageIO.write(SwingFXUtils.fromFXImage(node.snapshot(null, null), null), "png", // NON-NLS + reportPath.resolve(reportName + REPORT_IMAGE_EXTENSION).toFile()); // NON-NLS - //write html wrapper - try (Writer htmlWriter = new FileWriter(reportHTMLFIle)) { - htmlWriter.write(wrapper.toString()); - } catch (FileNotFoundException ex) { - LOGGER.log(Level.WARNING, "failed to open html wrapper file for writing ", ex); // NON-NLS - return; - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "failed to write html wrapper file", ex); // NON-NLS - return; - } + //copy report css + try (InputStream resource = this.getClass().getResourceAsStream("/org/sleuthkit/autopsy/timeline/index.css")) { // NON-NLS + Files.copy(resource, reportPath.resolve("index.css")); // NON-NLS + } - //copy css - try (InputStream resource = this.getClass().getResourceAsStream("/org/sleuthkit/autopsy/timeline/index.css")) { // NON-NLS - Files.copy(resource, Paths.get(reportPath, "index.css")); // NON-NLS - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "failed to copy css file", ex); // NON-NLS - } + //add html file as report to case + try { + Case.getCurrentCase().addReport(reportHTMLFIle.getPath(), Bundle.Timeline_ModuleName(), reportName + HTML_EXT); // NON-NLS + } catch (TskCoreException ex) { + LOGGER.log(Level.WARNING, "failed to add html wrapper as a report", ex); // NON-NLS + new Alert(Alert.AlertType.ERROR, Bundle.SaveSnapShotAsReport_FailedToAddReport()).showAndWait(); + } - //add html file as report to case - try { - Case.getCurrentCase().addReport(reportHTMLFIle.getPath(), "Timeline", reportName + HTML_EXT); // NON-NLS - } catch (TskCoreException ex) { - LOGGER.log(Level.WARNING, "failed add html wrapper as a report", ex); // NON-NLS + //create alert to notify user of report location + final Alert alert = new Alert(Alert.AlertType.INFORMATION, null, open, ok); + alert.setTitle(Bundle.SaveSnapshot_action_name_text()); + alert.setHeaderText(Bundle.SaveSnapShotAsReport_Success()); + alert.initStyle(StageStyle.UTILITY); + alert.initOwner(node.getScene().getWindow()); + alert.initModality(Modality.APPLICATION_MODAL); + + //make action to open report, and hyperlinklable to invoke action + final OpenReportAction openReportAction = new OpenReportAction(reportHTMLFIle); + HyperlinkLabel hyperlinkLabel = new HyperlinkLabel(Bundle.SaveSnapShotAsReport_ReportSavedAt(reportHTMLFIle.getPath())); + hyperlinkLabel.setOnAction(openReportAction); + alert.getDialogPane().setContent(hyperlinkLabel); + + alert.showAndWait().ifPresent(buttonType -> { + if (buttonType == open) { + openReportAction.handle(null); + } + }); + + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error writing report " + reportPath + " to disk", e); // NON-NLS + new Alert(Alert.AlertType.ERROR, Bundle.SaveSnapShotAsReport_ErrorWritingReport(reportPath)).showAndWait(); } } }); } + + private static void writeHTMLFile(String reportName, final Writer htmlWriter, ZoomParams zoomParams) throws IOException { + + //write html wrapper file + htmlWriter.write("\n\n\ttimeline snapshot\n\t\n\n\n"); // NON-NLS + htmlWriter.write("
\n

" + reportName + "

\n"); // NON-NLS + //embed snapshot + htmlWriter.write("\"snaphot\""); // NON-NLS + //write view paramaters + htmlWriter.write("\n"); // NON-NLS + writeTableRow(htmlWriter, "Case", Case.getCurrentCase().getName()); // NON-NLS + writeTableRow(htmlWriter, "Time Range", zoomParams.getTimeRange().toString()); // NON-NLS + writeTableRow(htmlWriter, "Description Level of Detail", zoomParams.getDescriptionLOD().getDisplayName()); // NON-NLS + writeTableRow(htmlWriter, "Event Type Zoom Level", zoomParams.getTypeZoomLevel().getDisplayName()); // NON-NLS + writeTableRow(htmlWriter, "Filters", zoomParams.getFilter().getHTMLReportString()); // NON-NLS + //end table and html + htmlWriter.write("
\n"); // NON-NLS + htmlWriter.write("
\n\n"); // NON-NLS + } + + /** + * + * @param htmlWriter the value of htmlWriter + * @param key the value of Key + * @param value the value of value + * + * @throws IOException + */ + private static void writeTableRow(final Writer htmlWriter, final String key, final String value) throws IOException { + htmlWriter.write("" + key + ": " + value + "\n"); // NON-NLS + } + + @NbBundle.Messages({"OpenReportAction.DisplayName=Open Report", + "OpenReportAction.NoAssociatedEditorMessage=There is no associated editor for reports of this type or the associated application failed to launch.", + "OpenReportAction.MessageBoxTitle=Open Report Failure", + "OpenReportAction.NoOpenInEditorSupportMessage=This platform (operating system) does not support opening a file in an editor this way.", + "OpenReportAction.MissingReportFileMessage=The report file no longer exists.", + "OpenReportAction.ReportFileOpenPermissionDeniedMessage=Permission to open the report file was denied."}) + private class OpenReportAction extends Action { + + OpenReportAction(File reportHTMLFIle) { + super(Bundle.OpenReportAction_DisplayName()); + setEventHandler(actionEvent -> { + try { + Desktop.getDesktop().open(reportHTMLFIle); + } catch (IOException ex) { + JOptionPane.showMessageDialog(null, + Bundle.OpenReportAction_NoAssociatedEditorMessage(), + Bundle.OpenReportAction_MessageBoxTitle(), + JOptionPane.ERROR_MESSAGE); + } catch (UnsupportedOperationException ex) { + JOptionPane.showMessageDialog(null, + Bundle.OpenReportAction_NoOpenInEditorSupportMessage(), + Bundle.OpenReportAction_MessageBoxTitle(), + JOptionPane.ERROR_MESSAGE); + } catch (IllegalArgumentException ex) { + JOptionPane.showMessageDialog(null, + Bundle.OpenReportAction_MissingReportFileMessage(), + Bundle.OpenReportAction_MessageBoxTitle(), + JOptionPane.ERROR_MESSAGE); + } catch (SecurityException ex) { + JOptionPane.showMessageDialog(null, + Bundle.OpenReportAction_ReportFileOpenPermissionDeniedMessage(), + Bundle.OpenReportAction_MessageBoxTitle(), + JOptionPane.ERROR_MESSAGE); + } + }); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java index 9ba8977757..52d64c0c1f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java @@ -72,13 +72,12 @@ import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent; * common history context menu items out of derived classes? -jm */ public abstract class AbstractVisualizationPane & TimeLineChart> extends BorderPane { - - @NbBundle.Messages("AbstractVisualization.Drag_Tooltip.text=Drag the mouse to select a time interval to zoom into.") - private static final Tooltip DRAG_TOOLTIP = new Tooltip(Bundle.AbstractVisualization_Drag_Tooltip_text()); + @NbBundle.Messages("AbstractVisualization.Default_Tooltip.text=Drag the mouse to select a time interval to zoom into.\nRight-click for more actions.") + private static final Tooltip DEFAULT_TOOLTIP = new Tooltip(Bundle.AbstractVisualization_Default_Tooltip_text()); private static final Logger LOGGER = Logger.getLogger(AbstractVisualizationPane.class.getName()); - public static Tooltip getDragTooltip() { - return DRAG_TOOLTIP; + public static Tooltip getDefaultTooltip() { + return DEFAULT_TOOLTIP; } protected final SimpleBooleanProperty hasEvents = new SimpleBooleanProperty(true); @@ -242,16 +241,16 @@ public abstract class AbstractVisualizationPane }); TimeLineController.getTimeZone().addListener(invalidationListener); - + //show tooltip text in status bar hoverProperty().addListener((observable, oldActivated, newActivated) -> { if (newActivated) { - controller.setStatus(DRAG_TOOLTIP.getText()); + controller.setStatus(DEFAULT_TOOLTIP.getText()); } else { controller.setStatus(""); } }); - + update(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/NoEventsDialog.fxml b/Core/src/org/sleuthkit/autopsy/timeline/ui/NoEventsDialog.fxml index 418df66a39..d4352a23b9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/NoEventsDialog.fxml +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/NoEventsDialog.fxml @@ -7,68 +7,94 @@ - - - + + + + + + + + + + - - - - - - - - - - - + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java index 6d0985eaf6..afa2f7a895 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java @@ -37,7 +37,6 @@ import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.MenuButton; -import javafx.scene.control.TitledPane; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToolBar; @@ -74,6 +73,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.VisualizationMode; +import org.sleuthkit.autopsy.timeline.actions.Back; import org.sleuthkit.autopsy.timeline.actions.ResetFilters; import org.sleuthkit.autopsy.timeline.actions.SaveSnapshotAsReport; import org.sleuthkit.autopsy.timeline.actions.ZoomIn; @@ -537,8 +537,10 @@ final public class VisualizationPanel extends BorderPane { } } - private class NoEventsDialog extends TitledPane { + private class NoEventsDialog extends StackPane { + @FXML + private Button backButton; @FXML private Button resetFiltersButton; @FXML @@ -562,13 +564,12 @@ final public class VisualizationPanel extends BorderPane { assert zoomButton != null : "fx:id=\"zoomButton\" was not injected: check your FXML file 'NoEventsDialog.fxml'."; // NON-NLS noEventsDialogLabel.setText(NbBundle.getMessage(NoEventsDialog.class, "VisualizationPanel.noEventsDialogLabel.text")); // NON-NLS - ActionUtils.configureButton(new ZoomToEvents(controller), zoomButton); - + dismissButton.setOnAction(actionEvent -> closeCallback.run()); - Action defaultFiltersAction = new ResetFilters(controller); - resetFiltersButton.setOnAction(defaultFiltersAction); - resetFiltersButton.disableProperty().bind(defaultFiltersAction.disabledProperty()); - resetFiltersButton.setText(NbBundle.getMessage(NoEventsDialog.class, "VisualizationPanel.resetFiltersButton.text")); // NON-NLS + + ActionUtils.configureButton(new ZoomToEvents(controller), zoomButton); + ActionUtils.configureButton(new Back(controller), backButton); + ActionUtils.configureButton(new ResetFilters(controller), resetFiltersButton); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java index f33e4e53fe..ff5a3d4798 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java @@ -267,7 +267,7 @@ public class CountsViewPane extends AbstractVisualizationPane(new CountsViewSettingsPane().getChildrenUnmodifiable()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties index 091926228a..77bc1dd893 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties @@ -1,4 +1,3 @@ -Timeline.ui.detailview.tooltip.text={0}\nRight-click to remove.\nRight-drag to reposition. DetailViewPane.loggedTask.name=Update Details DetailViewPane.loggedTask.preparing=preparing DetailViewPane.loggedTask.queryDb=querying db diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index a881b4ff5a..6a8e6355f2 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -54,8 +54,6 @@ import static javafx.scene.input.KeyCode.PAGE_DOWN; import static javafx.scene.input.KeyCode.PAGE_UP; import static javafx.scene.input.KeyCode.UP; import javafx.scene.input.KeyEvent; -import javafx.scene.input.MouseEvent; -import javafx.scene.input.ScrollEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; @@ -93,53 +91,92 @@ public class DetailViewPane extends AbstractVisualizationPane>> treeSelectionModel; + private static final double LINE_SCROLL_PERCENTAGE = .10; + private static final double PAGE_SCROLL_PERCENTAGE = .70; - //these three could be injected from fxml but it was causing npe's private final DateAxis dateAxis = new DateAxis(); - private final Axis verticalAxis = new EventAxis(); + private final ScrollBar vertScrollBar = new ScrollBar(); + private final Region scrollBarSpacer = new Region(); + + private MultipleSelectionModel>> treeSelectionModel; + private final ObservableList> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); //private access to barchart data private final Map> eventTypeToSeriesMap = new ConcurrentHashMap<>(); - private final ScrollBar vertScrollBar = new ScrollBar(); - - private final Region region = new Region(); - - private final ObservableList> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - public ObservableList> getEventBundles() { return chart.getEventBundles(); } + public DetailViewPane(TimeLineController controller, Pane partPane, Pane contextPane, Region bottomLeftSpacer) { + super(controller, partPane, contextPane, bottomLeftSpacer); - public DetailViewPane(TimeLineController controller, Pane partPane, Pane contextPane, Region spacer) { - super(controller, partPane, contextPane, spacer); + //initialize chart; chart = new EventDetailsChart(controller, dateAxis, verticalAxis, selectedNodes); - setChartClickHandler(); + setChartClickHandler(); //can we push this into chart chart.setData(dataSets); setCenter(chart); - - chart.setPrefHeight(USE_COMPUTED_SIZE); - settingsNodes = new ArrayList<>(new DetailViewSettingsPane().getChildrenUnmodifiable()); - vertScrollBar.setOrientation(Orientation.VERTICAL); - VBox vBox = new VBox(); - VBox.setVgrow(vertScrollBar, Priority.ALWAYS); - vBox.getChildren().add(vertScrollBar); - vBox.getChildren().add(region); - setRight(vBox); - + //bind layout fo axes and spacers + dateAxis.setTickLabelGap(0); dateAxis.setAutoRanging(false); - region.minHeightProperty().bind(dateAxis.heightProperty()); - vertScrollBar.visibleAmountProperty().bind(chart.heightProperty().multiply(100).divide(chart.maxVScrollProperty())); - requestLayout(); + dateAxis.setTickLabelsVisible(false); + dateAxis.getTickMarks().addListener((Observable observable) -> layoutDateLabels()); + dateAxis.getTickSpacing().addListener(observable -> layoutDateLabels()); + bottomLeftSpacer.minWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty())); + bottomLeftSpacer.prefWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty())); + bottomLeftSpacer.maxWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty())); + + scrollBarSpacer.minHeightProperty().bind(dateAxis.heightProperty()); + + //configure scrollbar + vertScrollBar.setOrientation(Orientation.VERTICAL); + vertScrollBar.maxProperty().bind(chart.maxVScrollProperty().subtract(chart.heightProperty())); + vertScrollBar.visibleAmountProperty().bind(chart.heightProperty()); + vertScrollBar.visibleProperty().bind(vertScrollBar.visibleAmountProperty().greaterThanOrEqualTo(0)); + VBox.setVgrow(vertScrollBar, Priority.ALWAYS); + setRight(new VBox(vertScrollBar, scrollBarSpacer)); + + //interpret scroll events to the scrollBar + this.setOnScroll(scrollEvent -> + vertScrollBar.valueProperty().set(clampScroll(vertScrollBar.getValue() - scrollEvent.getDeltaY()))); + + //request focus for keyboard scrolling + setOnMouseClicked(mouseEvent -> requestFocus()); + + //interpret scroll related keys to scrollBar + this.setOnKeyPressed((KeyEvent t) -> { + switch (t.getCode()) { + case PAGE_UP: + incrementScrollValue(-PAGE_SCROLL_PERCENTAGE); + t.consume(); + break; + case PAGE_DOWN: + incrementScrollValue(PAGE_SCROLL_PERCENTAGE); + t.consume(); + break; + case KP_UP: + case UP: + incrementScrollValue(-LINE_SCROLL_PERCENTAGE); + t.consume(); + break; + case KP_DOWN: + case DOWN: + incrementScrollValue(LINE_SCROLL_PERCENTAGE); + t.consume(); + break; + } + }); + + //scrollbar value change handler. This forwards changes in scroll bar to chart + this.vertScrollBar.valueProperty().addListener(observable -> chart.setVScroll(vertScrollBar.getValue())); + + //maintain highlighted effect on correct nodes highlightedNodes.addListener((ListChangeListener.Change> change) -> { - while (change.next()) { change.getAddedSubList().forEach(node -> { node.applyHighlightEffect(true); @@ -149,71 +186,24 @@ public class DetailViewPane extends AbstractVisualizationPane { - requestFocus(); - }); - - //These scroll related handlers don't affect any other view or the model, so they are handled internally - //mouse wheel scroll handler - this.onScrollProperty().set((ScrollEvent t) -> { - vertScrollBar.valueProperty().set(Math.max(0, Math.min(100, vertScrollBar.getValue() - t.getDeltaY() / 200.0))); - }); - - this.setOnKeyPressed((KeyEvent t) -> { - switch (t.getCode()) { - case PAGE_UP: - incrementScrollValue(-70); - break; - case PAGE_DOWN: - incrementScrollValue(70); - break; - case KP_UP: - case UP: - incrementScrollValue(-10); - break; - case KP_DOWN: - case DOWN: - incrementScrollValue(10); - break; - } - t.consume(); - }); - - //scrollbar handler - this.vertScrollBar.valueProperty().addListener((o, oldValue, newValue) -> { - chart.setVScroll(newValue.doubleValue() / 100.0); - }); - spacer.minWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty())); - spacer.prefWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty())); - spacer.maxWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty())); - - dateAxis.setTickLabelsVisible(false); - - dateAxis.getTickMarks().addListener((Observable observable) -> { - layoutDateLabels(); - }); - dateAxis.getTickSpacing().addListener((Observable observable) -> { - layoutDateLabels(); - }); - - dateAxis.setTickLabelGap(0); selectedNodes.addListener((Observable observable) -> { highlightedNodes.clear(); selectedNodes.stream().forEach((tn) -> { - for (EventBundleNodeBase n : chart.getNodes((EventBundleNodeBase t) -> t.getDescription().equals(tn.getDescription()))) { highlightedNodes.add(n); } }); }); - } - private void incrementScrollValue(int factor) { - vertScrollBar.valueProperty().set(Math.max(0, Math.min(100, vertScrollBar.getValue() + factor * (chart.getHeight() / chart.maxVScrollProperty().get())))); + private void incrementScrollValue(double factor) { + vertScrollBar.valueProperty().set(clampScroll(vertScrollBar.getValue() + factor * chart.getHeight())); + } + + private Double clampScroll(Double value) { + return Math.max(0, Math.min(vertScrollBar.getMax() + 50, value)); } public void setSelectionModel(MultipleSelectionModel>> selectionModel) { @@ -409,7 +399,7 @@ public class DetailViewPane extends AbstractVisualizationPane implements TimeLineChart { + private static final String styleSheet = GuideLine.class.getResource("EventsDetailsChart.css").toExternalForm(); private static final Image HIDE = new Image("/org/sleuthkit/autopsy/timeline/images/eye--minus.png"); // NON-NLS private static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS private static final Image MARKER = new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true); @@ -109,6 +111,7 @@ public final class EventDetailsChart extends XYChart imp private ContextMenu chartContextMenu; + @Override public ContextMenu getChartContextMenu() { return chartContextMenu; } @@ -116,6 +119,7 @@ public final class EventDetailsChart extends XYChart imp /** * a user positionable vertical line to help compare events */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private Line guideLine; /** @@ -193,6 +197,13 @@ public final class EventDetailsChart extends XYChart imp this.controller = controller; this.filteredEvents = this.controller.getEventsModel(); + sceneProperty().addListener(observable -> { + Scene scene = getScene(); + if (scene != null && scene.getStylesheets().contains(styleSheet) == false) { + scene.getStylesheets().add(styleSheet); + } + }); + filteredEvents.zoomParametersProperty().addListener(o -> { clearGuideLine(); clearIntervalSelector(); @@ -200,15 +211,14 @@ public final class EventDetailsChart extends XYChart imp projectionMap.clear(); controller.selectEventIDs(Collections.emptyList()); }); - Tooltip.install(this, AbstractVisualizationPane.getDragTooltip()); + + Tooltip.install(this, AbstractVisualizationPane.getDefaultTooltip()); dateAxis.setAutoRanging(false); - verticalAxis.setVisible(false);//TODO: why doesn't this hide the vertical axis, instead we have to turn off all parts individually? -jm verticalAxis.setTickLabelsVisible(false); verticalAxis.setTickMarkVisible(false); setLegendVisible(false); - setPadding(Insets.EMPTY); setAlternativeColumnFillVisible(true); @@ -425,12 +435,11 @@ public final class EventDetailsChart extends XYChart imp return getNodes(x -> true); } - synchronized void setVScroll(double d) { - final double h = maxY.get() - (getHeight() * .9); - nodeGroup.setTranslateY(-d * h); + synchronized void setVScroll(double vScrollValue) { + nodeGroup.setTranslateY(-vScrollValue); } - private void clearGuideLine() { + void clearGuideLine() { getChartChildren().remove(guideLine); guideLine = null; } @@ -612,16 +621,10 @@ public final class EventDetailsChart extends XYChart imp setGraphic(new ImageView(MARKER)); // NON-NLS setEventHandler(actionEvent -> { if (guideLine == null) { - guideLine = new GuideLine(0, 0, 0, getHeight(), getXAxis()); + guideLine = new GuideLine(EventDetailsChart.this); guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); - guideLine.endYProperty().bind(heightProperty().subtract(getXAxis().heightProperty().subtract(getXAxis().tickLengthProperty()))); getChartChildren().add(guideLine); - guideLine.setOnMouseClicked(mouseEvent -> { - if (mouseEvent.getButton() == MouseButton.SECONDARY) { - clearGuideLine(); - mouseEvent.consume(); - } - }); + } else { guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventsDetailsChart.css b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventsDetailsChart.css new file mode 100644 index 0000000000..c89d12a374 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventsDetailsChart.css @@ -0,0 +1,7 @@ +.guide-line{ + -fx-opacity: .5; + -fx-stroke: red; + -fx-stroke-dash-array: 5 5; + -fx-stroke-width: 3; + -fx-cursor: h-resize; +} \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java index 3a2dad163b..0c8cbfeffd 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2014-15 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,66 +18,69 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; -import javafx.scene.Cursor; import javafx.scene.chart.Axis; import javafx.scene.control.Tooltip; -import javafx.scene.input.MouseEvent; -import javafx.scene.paint.Color; +import javafx.scene.input.MouseButton; import javafx.scene.shape.Line; import org.joda.time.DateTime; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane; /** - * + * Subclass of {@link Line} with appropriate behavior (mouse listeners) to act + * as a visual reference point in the details view. */ +@NbBundle.Messages({"# {0} - date/time at guideline position", + "GuideLine.tooltip.text={0}\nRight-click to remove.\nDrag to reposition."}) class GuideLine extends Line { - private final Axis dateAxis; + private static final Tooltip CHART_DEFAULT_TOOLTIP = AbstractVisualizationPane.getDefaultTooltip(); + private final Tooltip tooltip = new Tooltip(); + private final EventDetailsChart chart; + + //used across invocations of mouse event handlers to maintain state private double startLayoutX; - - protected Tooltip tooltip; - private double dragStartX = 0; - GuideLine(double startX, double startY, double endX, double endY, Axis axis) { - super(startX, startY, endX, endY); - dateAxis = axis; - setCursor(Cursor.E_RESIZE); - getStrokeDashArray().setAll(5.0, 5.0); - setStroke(Color.RED); - setOpacity(.5); - setStrokeWidth(3); + /** + * @param chart the chart this GuideLine belongs to. + */ + GuideLine(EventDetailsChart chart) { + super(0, 0, 0, 0); + this.chart = chart; + Axis xAxis = chart.getXAxis(); + endYProperty().bind(chart.heightProperty().subtract(xAxis.heightProperty().subtract(xAxis.tickLengthProperty()))); - setOnMouseEntered((MouseEvent event) -> { - setTooltip(); - }); + getStyleClass().add("guide-line"); - setOnMousePressed((MouseEvent event) -> { - startLayoutX = getLayoutX(); - dragStartX = event.getScreenX(); - }); - setOnMouseDragged((MouseEvent event) -> { - double dX = event.getScreenX() - dragStartX; - - relocate(startLayoutX + dX, 0); - }); - } - - private void setTooltip() { - Tooltip.uninstall(this, tooltip); - tooltip = new Tooltip( - NbBundle.getMessage(this.getClass(), "Timeline.ui.detailview.tooltip.text", formatSpan(getDateTime()))); Tooltip.install(this, tooltip); + tooltip.setOnShowing(showing -> tooltip.setText(Bundle.GuideLine_tooltip_text(getDateTimeAsString()))); + + //this is a hack to override the tooltip of the enclosing chart. + setOnMouseEntered(entered -> Tooltip.uninstall(chart, CHART_DEFAULT_TOOLTIP)); + setOnMouseExited(exited -> Tooltip.install(chart, CHART_DEFAULT_TOOLTIP)); + + setOnMouseClicked(clickedEvent -> { + if (clickedEvent.getButton() == MouseButton.SECONDARY + && clickedEvent.isStillSincePress() == false) { + chart.clearGuideLine(); + clickedEvent.consume(); + } + }); + setOnMousePressed(pressedEvent -> { + startLayoutX = getLayoutX(); + dragStartX = pressedEvent.getScreenX(); + }); + setOnMouseDragged(dragEvent -> { + double dX = dragEvent.getScreenX() - dragStartX; + relocate(startLayoutX + dX, 0); + dragEvent.consume(); + }); } - private String formatSpan(DateTime date) { - return date.toString(TimeLineController.getZonedFormatter()); + private String getDateTimeAsString() { + return chart.getDateTimeForPosition(getLayoutX()).toString(TimeLineController.getZonedFormatter()); } - - private DateTime getDateTime() { - return dateAxis.getValueForDisplay(dateAxis.parentToLocal(getLayoutX(), 0).getX()); - } - }