diff --git a/Core/ivy.xml b/Core/ivy.xml index 6d67fb0992..4250c385a4 100644 --- a/Core/ivy.xml +++ b/Core/ivy.xml @@ -23,5 +23,7 @@ + + diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties index e4903c6ab3..b8360a3b05 100644 --- a/Core/nbproject/project.properties +++ b/Core/nbproject/project.properties @@ -3,6 +3,7 @@ file.reference.c3p0-0.9.5.jar=release/modules/ext/c3p0-0.9.5.jar file.reference.commons-compress-1.14.jar=release/modules/ext/commons-compress-1.14.jar file.reference.commons-dbcp2-2.1.1.jar=release\\modules\\ext\\commons-dbcp2-2.1.1.jar file.reference.commons-pool2-2.4.2.jar=release\\modules\\ext\\commons-pool2-2.4.2.jar +file.reference.dd-plist-1.20.jar=release\\modules\\ext\\dd-plist-1.20.jar file.reference.jdom-2.0.5-contrib.jar=release/modules/ext/jdom-2.0.5-contrib.jar file.reference.jdom-2.0.5.jar=release/modules/ext/jdom-2.0.5.jar file.reference.jsoup-1.10.3.jar=release/modules/ext/jsoup-1.10.3.jar diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml index 53471a3396..bfcd75f7ba 100644 --- a/Core/nbproject/project.xml +++ b/Core/nbproject/project.xml @@ -405,6 +405,10 @@ ext/Rejistry-1.0-SNAPSHOT.jar release/modules/ext/Rejistry-1.0-SNAPSHOT.jar + + ext/dd-plist-1.20.jar + release/modules/ext/dd-plist-1.20.jar + ext/sevenzipjbinding-AllPlatforms.jar release/modules/ext/sevenzipjbinding-AllPlatforms.jar diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties index 68e938a107..f1bbf775af 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties @@ -43,3 +43,4 @@ SQLiteViewer.currPageLabel.text=x SQLiteViewer.jLabel2.text=Page SQLiteViewer.numEntriesField.text=num Entries SQLiteViewer.jLabel1.text=Table +PListViewer.exportButton.text=Export diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/EpochTimeCellRenderer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/EpochTimeCellRenderer.java new file mode 100644 index 0000000000..723c1ef0f3 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/EpochTimeCellRenderer.java @@ -0,0 +1,105 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers; + +import java.awt.Component; +import java.awt.Font; +import java.lang.reflect.InvocationTargetException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.logging.Level; +import javax.swing.JTable; +import javax.swing.table.DefaultTableCellRenderer; +import org.openide.nodes.Node; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * Custom Cell renderer to display a SQLite column cell as readable Epoch date/time + * + */ +public class EpochTimeCellRenderer extends DefaultTableCellRenderer { + + private static final long serialVersionUID = 1L; + private static final Logger LOGGER = Logger.getLogger(FileViewer.class.getName()); + + private static final String FORMAT_STRING = "yyyy/MM/dd HH:mm:ss"; //NON-NLS + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat(FORMAT_STRING); + + private final boolean renderAsEpoch; + + EpochTimeCellRenderer(boolean renderAsEpoch) { + this.renderAsEpoch = renderAsEpoch; + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + + // Set the forceground/background so its obvious when the cell is selected. + if (isSelected) { + super.setForeground(table.getSelectionForeground()); + super.setBackground(table.getSelectionBackground()); + } else { + super.setForeground( table.getForeground()); + super.setBackground( table.getBackground()); + } + + if (value == null) { + setText(""); + } + else { + String textStr = ""; + try { + // get the col property value + if (value instanceof Node.Property) { + Node.Property nodeProp = (Node.Property)value; + textStr = nodeProp.getValue().toString(); + } + + if (renderAsEpoch) { + long epochTime = Long.parseUnsignedLong(textStr); + if (epochTime > 0 ) { + Font font = getFont(); + setFont(font.deriveFont(font.getStyle() | Font.ITALIC)); + setText(DATE_FORMAT.format(new Date(epochTime))); + } + else { + setText(textStr); + } + } + else { // Display raw data + setText(textStr); + } + } + catch (NumberFormatException e) { + setText(textStr); + LOGGER.log(Level.INFO, "Error converting column value to number.", e); //NON-NLS + } catch (IllegalAccessException | InvocationTargetException ex) { + setText(""); + LOGGER.log(Level.SEVERE, "Error in getting column value.", ex); //NON-NLS + } + } + + return this; + } + + boolean isRenderingAsEpoch() { + return this.renderAsEpoch; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/FileViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/FileViewer.java index db13e523e5..d42d834395 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/FileViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/FileViewer.java @@ -46,7 +46,8 @@ public class FileViewer extends javax.swing.JPanel implements DataContentViewer // TBD: This hardcoded list of viewers should be replaced with a dynamic lookup private static final FileTypeViewer[] KNOWN_VIEWERS = new FileTypeViewer[]{ // new JPEGViewerDummy(), // this if for testing only - new SQLiteViewer() + new SQLiteViewer(), + new PListViewer() }; private FileTypeViewer lastViewer; diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/PListRowFactory.java b/Core/src/org/sleuthkit/autopsy/contentviewers/PListRowFactory.java new file mode 100644 index 0000000000..75b2970ecb --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/PListRowFactory.java @@ -0,0 +1,149 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.ChildFactory; +import org.openide.nodes.Children; +import org.openide.nodes.Node; +import org.openide.nodes.Sheet; +import org.sleuthkit.autopsy.contentviewers.PListViewer.PropKeyValue; +import org.sleuthkit.autopsy.contentviewers.PListViewer.PropertyType; +import org.sleuthkit.autopsy.datamodel.NodeProperty; + +/** + * Factory class to create nodes for Plist table view + */ +public class PListRowFactory extends ChildFactory { + + private final List rows; + + PListRowFactory(final List rows) { + this.rows = rows; + } + + /** + * Creates keys + * + * @param keys + * @return true + */ + @Override + protected boolean createKeys(final List keys) { + if (rows != null) { + for (int i = 0; i < rows.size(); i++) { + keys.add(i); + } + } + return true; + } + + /** + * Creates node for the given key + * @param key + * @return node for the given key, null if the key is invalid or node doesn't exist + */ + @Override + protected Node createNodeForKey(final Integer key) { + if (Objects.isNull(rows) || rows.isEmpty() || key >= rows.size()) { + return null; + } + return new PListNode(rows.get(key)); + } +} + +/** + * Node for a Plist key +*/ +class PListNode extends AbstractNode { + + private final PropKeyValue propKeyVal; + + PListNode(final PropKeyValue propKeyVal) { + + super(propKeyVal.getChildren() == null ? Children.LEAF : new PListNodeChildren(propKeyVal.getChildren())); + + this.propKeyVal = propKeyVal; + + super.setName(propKeyVal.getKey()); + super.setDisplayName(propKeyVal.getKey()); + if (propKeyVal.getType() == PropertyType.ARRAY) { + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/keychain-16.png"); + } else if (propKeyVal.getType() == PropertyType.DICTIONARY) { + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/keys-dict-16.png"); + } else { + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/key-16.png"); + } + + } + + /** + * Creates property sheet for the node + */ + @Override + protected Sheet createSheet() { + + final Sheet sheet = super.createSheet(); + Sheet.Set properties = sheet.get(Sheet.PROPERTIES); + if (properties == null) { + properties = Sheet.createPropertiesSet(); + sheet.put(properties); + } + + properties.put(new NodeProperty<>(Bundle.PListNode_TypeCol(), + Bundle.PListNode_TypeCol(), + Bundle.PListNode_TypeCol(), + propKeyVal.getType().name())); // NON-NLS + + properties.put(new NodeProperty<>(Bundle.PListNode_ValueCol(), + Bundle.PListNode_ValueCol(), + Bundle.PListNode_ValueCol(), + (propKeyVal.getChildren() == null) ? propKeyVal.getValue() : "")); // NON-NLS + + return sheet; + } + + /** + * Creates children nodes for a compound PList key + */ + private static class PListNodeChildren extends Children.Keys { + + private final List children; + + PListNodeChildren(final PropKeyValue... children) { + super(); + this.children = Arrays.asList(children); + } + + @Override + protected void addNotify() { + super.setKeys(this.children); + } + + @Override + protected Node[] createNodes(final PropKeyValue propKeyVal) { + return new Node[]{new PListNode(propKeyVal)}; + } + + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/PListViewer.form b/Core/src/org/sleuthkit/autopsy/contentviewers/PListViewer.form new file mode 100644 index 0000000000..bc39dde617 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/PListViewer.form @@ -0,0 +1,95 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/PListViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/PListViewer.java new file mode 100644 index 0000000000..ff31273dd6 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/PListViewer.java @@ -0,0 +1,498 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers; + +import java.awt.Component; +import java.util.List; +import org.sleuthkit.datamodel.AbstractFile; +import java.util.Arrays; +import com.dd.plist.NSDictionary; +import com.dd.plist.PropertyListParser; +import com.dd.plist.NSObject; +import com.dd.plist.NSArray; +import com.dd.plist.NSDate; +import com.dd.plist.NSString; +import com.dd.plist.NSNumber; +import com.dd.plist.NSData; +import com.dd.plist.PropertyListFormatException; +import java.io.File; +import java.io.IOException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.logging.Level; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.SwingWorker; +import javax.swing.filechooser.FileNameExtensionFilter; +import javax.swing.table.TableCellRenderer; +import javax.xml.parsers.ParserConfigurationException; +import org.netbeans.swing.outline.DefaultOutlineModel; +import org.netbeans.swing.outline.Outline; +import org.openide.explorer.ExplorerManager; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.Children; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.TskCoreException; +import org.xml.sax.SAXException; + +/** + * PListViewer - a file viewer for binary plist files. + * + */ +public class PListViewer extends javax.swing.JPanel implements FileTypeViewer, ExplorerManager.Provider { + + private static final long serialVersionUID = 1L; + private static final String[] MIMETYPES = new String[]{"application/x-bplist"}; + private static final Logger LOGGER = Logger.getLogger(PListViewer.class.getName()); + + private final org.openide.explorer.view.OutlineView outlineView; + private final Outline outline; + private ExplorerManager explorerManager; + + private NSDictionary rootDict; + + /** + * Creates new form PListViewer + */ + public PListViewer() { + + // Create an Outlineview and add to the panel + outlineView = new org.openide.explorer.view.OutlineView(); + + initComponents(); + + outline = outlineView.getOutline(); + + ((DefaultOutlineModel) outline.getOutlineModel()).setNodesColumnLabel("Key"); + + outlineView.setPropertyColumns( + "Type", Bundle.PListNode_TypeCol(), + "Value", Bundle.PListNode_ValueCol()); + + customize(); + } + + @NbBundle.Messages({"PListNode.KeyCol=Key", + "PListNode.TypeCol=Type", + "PListNode.ValueCol=Value"}) + + private void customize() { + + outline.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + outline.setRootVisible(false); + if (null == explorerManager) { + explorerManager = new ExplorerManager(); + } + + //outline.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + plistTableScrollPane.setViewportView(outlineView); + + outline.setAutoResizeMode(JTable.AUTO_RESIZE_NEXT_COLUMN); + + this.setVisible(true); + outline.setRowSelectionAllowed(false); + } + + /** + * 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() { + + jPanel1 = new javax.swing.JPanel(); + plistTableScrollPane = new javax.swing.JScrollPane(); + hdrPanel = new javax.swing.JPanel(); + exportButton = new javax.swing.JButton(); + + jPanel1.setLayout(new java.awt.BorderLayout()); + + plistTableScrollPane.setBorder(null); + plistTableScrollPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + plistTableScrollPane.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); + jPanel1.add(plistTableScrollPane, java.awt.BorderLayout.CENTER); + + org.openide.awt.Mnemonics.setLocalizedText(exportButton, org.openide.util.NbBundle.getMessage(PListViewer.class, "PListViewer.exportButton.text")); // NOI18N + exportButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + exportButtonActionPerformed(evt); + } + }); + + javax.swing.GroupLayout hdrPanelLayout = new javax.swing.GroupLayout(hdrPanel); + hdrPanel.setLayout(hdrPanelLayout); + hdrPanelLayout.setHorizontalGroup( + hdrPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, hdrPanelLayout.createSequentialGroup() + .addContainerGap(320, Short.MAX_VALUE) + .addComponent(exportButton) + .addContainerGap()) + ); + hdrPanelLayout.setVerticalGroup( + hdrPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, hdrPanelLayout.createSequentialGroup() + .addGap(0, 6, Short.MAX_VALUE) + .addComponent(exportButton)) + ); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(hdrPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGap(5, 5, 5)) + .addComponent(jPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(3, 3, 3) + .addComponent(hdrPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(jPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, 249, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + + @NbBundle.Messages({"PListViewer.ExportSuccess.message=Plist file exported successfully", + "PListViewer.ExportFailed.message=Plist file export failed.",}) + + /** + * Handles the Export button pressed action + */ + private void exportButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportButtonActionPerformed + + final JFileChooser fileChooser = new JFileChooser(); + fileChooser.setCurrentDirectory(new File(Case.getCurrentCase().getExportDirectory())); + fileChooser.setFileFilter(new FileNameExtensionFilter("XML file", "xml")); + + final int returnVal = fileChooser.showSaveDialog(this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + + File selectedFile = fileChooser.getSelectedFile(); + if (!selectedFile.getName().endsWith(".xml")) { // NON-NLS + selectedFile = new File(selectedFile.toString() + ".xml"); // NON-NLS + } + + try { + //Save the propery list as XML + PropertyListParser.saveAsXML(this.rootDict, selectedFile); + JOptionPane.showMessageDialog(this, + String.format("Plist file exported successfully to %s ", selectedFile.getName()), + Bundle.PListViewer_ExportSuccess_message(), + JOptionPane.INFORMATION_MESSAGE); + } catch (IOException ex) { + JOptionPane.showMessageDialog(this, + String.format("Failed to export plist file to %s ", selectedFile.getName()), + Bundle.PListViewer_ExportFailed_message(), + JOptionPane.ERROR_MESSAGE); + + LOGGER.log(Level.SEVERE, "Error exporting plist to XML file " + selectedFile.getName(), ex); + } + } + }//GEN-LAST:event_exportButtonActionPerformed + + /** + * Returns mime types supported by this viewer + * + * @return list of supported mime types + */ + @Override + public List getSupportedMIMETypes() { + return Arrays.asList(MIMETYPES); + } + + /** + * Sets the file to be displayed in the viewer + * + * @param file file to display + */ + @Override + public void setFile(final AbstractFile file) { + processPlist(file); + } + + /** + * Returns the viewer component + * + * @return the viewer component + */ + @Override + public Component getComponent() { + return this; + } + + /** + * Resets the viewer component + * + */ + @Override + public void resetComponent() { + rootDict = null; + } + + /** + * Process the given Plist file + * + * @param plistFile - + * + * @return none + */ + private void processPlist(final AbstractFile plistFile) { + + final byte[] plistFileBuf = new byte[(int) plistFile.getSize()]; + try { + plistFile.read(plistFileBuf, 0, plistFile.getSize()); + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Error reading bytes of plist file.", ex); + } + + final List plist; + try { + plist = parsePList(plistFileBuf); + new SwingWorker() { + @Override + protected Void doInBackground() { + setupTable(plist); + return null; + } + + @Override + protected void done() { + super.done(); + setColumnWidths(); + } + }.execute(); + + } catch (IOException | PropertyListFormatException | ParseException | ParserConfigurationException | SAXException ex) { + LOGGER.log(Level.SEVERE, String.format("Error parsing plist for file (obj_id = %d)", plistFile.getId()), ex); + } + } + + /** + * Sets up the columns in the display table + * + * @param tableRows + */ + private void setupTable(final List tableRows) { + explorerManager.setRootContext(new AbstractNode(Children.create(new PListRowFactory(tableRows), true))); + } + + /** + * Sets up the column widths + * + */ + private void setColumnWidths() { + final int margin = 4; + final int padding = 8; + + // find the maximum width needed to fit the values for the first N rows, at most + final int rows = Math.min(20, outline.getRowCount()); + for (int col = 0; col < outline.getColumnCount(); col++) { + final int columnWidthLimit = 2000; + int columnWidth = 0; + + for (int row = 0; row < rows; row++) { + final TableCellRenderer renderer = outline.getCellRenderer(row, col); + final Component comp = outline.prepareRenderer(renderer, row, col); + + columnWidth = Math.max(comp.getPreferredSize().width, columnWidth); + } + + columnWidth += 2 * margin + padding; // add margin and regular padding + columnWidth = Math.min(columnWidth, columnWidthLimit); + outline.getColumnModel().getColumn(col).setPreferredWidth(columnWidth); + } + } + + /** + * Parses the given plist key/value + */ + @NbBundle.Messages({"PListViewer.DataType.message=Binary Data value not shown"}) + private PropKeyValue parseProperty(final String key, final NSObject value) { + if (value == null) { + return null; + } else if (value instanceof NSString) { + return new PropKeyValue(key, PropertyType.STRING, value.toString()); + } else if (value instanceof NSNumber) { + final NSNumber number = (NSNumber) value; + if (number.isInteger()) { + return new PropKeyValue(key, PropertyType.NUMBER, number.longValue()); + } else if (number.isBoolean()) { + return new PropKeyValue(key, PropertyType.BOOLEAN, number.boolValue()); + } else { + return new PropKeyValue(key, PropertyType.NUMBER, number.floatValue()); + } + } else if (value instanceof NSDate) { + final NSDate date = (NSDate) value; + return new PropKeyValue(key, PropertyType.DATE, date.toString()); + } else if (value instanceof NSData) { + return new PropKeyValue(key, PropertyType.DATA, Bundle.PListViewer_DataType_message()); + } else if (value instanceof NSArray) { + final List children = new ArrayList<>(); + final NSArray array = (NSArray) value; + + final PropKeyValue pkv = new PropKeyValue(key, PropertyType.ARRAY, array); + for (int i = 0; i < array.count(); i++) { + children.add(parseProperty("", array.objectAtIndex(i))); + } + + pkv.setChildren(children.toArray(new PropKeyValue[children.size()])); + return pkv; + } else if (value instanceof NSDictionary) { + final List children = new ArrayList<>(); + final NSDictionary dict = (NSDictionary) value; + + final PropKeyValue pkv = new PropKeyValue(key, PropertyType.DICTIONARY, dict); + for (final String key2 : ((NSDictionary) value).allKeys()) { + final NSObject obj = ((NSDictionary) value).objectForKey(key2); + children.add(parseProperty(key2, obj)); + } + + pkv.setChildren(children.toArray(new PropKeyValue[children.size()])); + return pkv; + } else { + LOGGER.log(Level.SEVERE, "Can''t parse Plist for key = {0} value of type {1}", new Object[]{key, value.getClass()}); + } + + return null; + } + + /** + * Parses given binary stream and extracts Plist key/value + * + * @param plistbytes + * + * @return list of PropKeyValue + */ + private List parsePList(final byte[] plistbytes) throws IOException, PropertyListFormatException, ParseException, ParserConfigurationException, SAXException { + + final List plist = new ArrayList<>(); + rootDict = (NSDictionary) PropertyListParser.parse(plistbytes); + + final String[] keys = rootDict.allKeys(); + for (final String key : keys) { + final PropKeyValue pkv = parseProperty(key, rootDict.objectForKey(key)); + if (null != pkv) { + plist.add(pkv); + } + } + + return plist; + } + + @Override + public ExplorerManager getExplorerManager() { + return explorerManager; + } + + /** + * Plist property type + */ + enum PropertyType { + STRING, + NUMBER, + BOOLEAN, + DATE, + DATA, + ARRAY, + DICTIONARY + }; + + /** + * Encapsulates a Plist property + * + */ + final static class PropKeyValue { + + private final String key; + private final PropertyType type; + private final Object value; + + private PropKeyValue[] children; + + PropKeyValue(String key, PropertyType type, Object value) { + this.key = key; + this.type = type; + this.value = value; + + this.children = null; + } + + /** + * Copy constructor + */ + PropKeyValue(PropKeyValue other) { + this.key = other.getKey(); + this.type = other.getType(); + this.value = other.getValue(); + + this.setChildren(other.getChildren()); + } + + String getKey() { + return this.key; + } + + PropertyType getType() { + return this.type; + } + + Object getValue() { + return this.value; + } + + /** + * Returns an array of children, if any. + * + * @return + */ + PropKeyValue[] getChildren() { + if (children == null) { + return null; + } + + // return a copy + return Arrays.stream(children) + .map(child -> new PropKeyValue(child)) + .toArray(PropKeyValue[]::new); + } + + void setChildren(final PropKeyValue... children) { + this.children = Arrays.stream(children) + .map(child -> new PropKeyValue(child)) + .toArray(PropKeyValue[]::new); + } + + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton exportButton; + private javax.swing.JPanel hdrPanel; + private javax.swing.JPanel jPanel1; + private javax.swing.JScrollPane plistTableScrollPane; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/SQLiteTableRowFactory.java b/Core/src/org/sleuthkit/autopsy/contentviewers/SQLiteTableRowFactory.java index 633f40260c..02a08f7037 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/SQLiteTableRowFactory.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/SQLiteTableRowFactory.java @@ -18,9 +18,12 @@ */ package org.sleuthkit.autopsy.contentviewers; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; +import javax.swing.Action; import org.openide.nodes.AbstractNode; import org.openide.nodes.ChildFactory; import org.openide.nodes.Children; @@ -28,12 +31,17 @@ import org.openide.nodes.Node; import org.openide.nodes.Sheet; import org.sleuthkit.autopsy.datamodel.NodeProperty; +/** + * Factory class to generate nodes for SQLite table rows + */ public class SQLiteTableRowFactory extends ChildFactory { private final List> rows; + private final List colActions; - public SQLiteTableRowFactory(List> rows) { + SQLiteTableRowFactory(List> rows, List actions ) { this.rows = rows; + this.colActions = actions; } @Override @@ -52,37 +60,53 @@ public class SQLiteTableRowFactory extends ChildFactory { return null; } - return new SQLiteTableRowNode(rows.get(key)); + return new SQLiteTableRowNode(rows.get(key), this.colActions ); } } +/** + * + * Node for SQLite table row + */ class SQLiteTableRowNode extends AbstractNode { private final Map row; - - SQLiteTableRowNode(Map row) { + private final List nodeActions; + + SQLiteTableRowNode(Map row, List actions) { super(Children.LEAF); this.row = row; + this.nodeActions = actions; } @Override protected Sheet createSheet() { - Sheet s = super.createSheet(); - Sheet.Set properties = s.get(Sheet.PROPERTIES); + Sheet sheet = super.createSheet(); + Sheet.Set properties = sheet.get(Sheet.PROPERTIES); if (properties == null) { properties = Sheet.createPropertiesSet(); - s.put(properties); + sheet.put(properties); } for (Map.Entry col : row.entrySet()) { String colName = col.getKey(); String colVal = col.getValue().toString(); - properties.put(new NodeProperty<>(colName, colName, colName, colVal)); // NON-NLS } - return s; + return sheet; } + + @Override + public Action[] getActions(boolean context) { + List actions = new ArrayList<>(); + + actions.addAll(Arrays.asList(super.getActions(context))); + actions.addAll(nodeActions); + + return actions.toArray(new Action[actions.size()]); + } + } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/SQLiteTableView.java b/Core/src/org/sleuthkit/autopsy/contentviewers/SQLiteTableView.java index 7ca873e13c..77c0814c55 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/SQLiteTableView.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/SQLiteTableView.java @@ -20,15 +20,22 @@ package org.sleuthkit.autopsy.contentviewers; import java.awt.BorderLayout; import java.awt.Component; +import java.awt.event.ActionEvent; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JMenu; +import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.ScrollPaneConstants; import javax.swing.SwingWorker; import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import org.netbeans.swing.etable.ETableColumn; import org.netbeans.swing.etable.ETableColumnModel; @@ -36,7 +43,12 @@ import org.netbeans.swing.outline.Outline; import org.openide.explorer.ExplorerManager; import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; +import org.openide.util.NbBundle; +import org.openide.util.actions.Presenter; +/** + * Panel to display a SQLite table + */ class SQLiteTableView extends JPanel implements ExplorerManager.Provider { private final org.openide.explorer.view.OutlineView outlineView; @@ -63,6 +75,7 @@ class SQLiteTableView extends JPanel implements ExplorerManager.Provider { outline.setRowSelectionAllowed(false); outline.setRootVisible(false); + outline.setCellSelectionEnabled(true); explorerManager = new ExplorerManager(); } @@ -71,6 +84,10 @@ class SQLiteTableView extends JPanel implements ExplorerManager.Provider { * * @param tableRows */ + @NbBundle.Messages({"SQLiteTableView.DisplayAs.text=Display as", + "SQLiteTableView.DisplayAsMenuItem.Date=Date", + "SQLiteTableView.DisplayAsMenuItem.RawData=Raw Data" + }) void setupTable(List> tableRows) { @@ -103,7 +120,11 @@ class SQLiteTableView extends JPanel implements ExplorerManager.Provider { @Override protected Boolean doInBackground() throws Exception { - explorerManager.setRootContext(new AbstractNode(Children.create(new SQLiteTableRowFactory(tableRows), true))); + List nodeActions = new ArrayList<>(); + + nodeActions.add(new ParseColAction(Bundle.SQLiteTableView_DisplayAs_text(), outline) ); + + explorerManager.setRootContext(new AbstractNode(Children.create(new SQLiteTableRowFactory(tableRows, nodeActions), true))); return false; } @@ -160,4 +181,79 @@ class SQLiteTableView extends JPanel implements ExplorerManager.Provider { // Variables declaration - do not modify//GEN-BEGIN:variables // End of variables declaration//GEN-END:variables + + + /** + * Action to handle "Display as" menu item. + * + */ + private class ParseColAction extends AbstractAction implements Presenter.Popup { + private final Outline outline; + private final String displayName; + + ParseColAction(String displayName, Outline outline ) { + super(displayName); + this.outline = outline; + this.displayName = displayName; + } + + @Override + public void actionPerformed(ActionEvent e) { + + } + + @Override + public JMenuItem getPopupPresenter() { + return new DisplayColAsMenu(); + } + + /** + * Class to SubMenu for "Display As" menu + */ + private class DisplayColAsMenu extends JMenu { + + DisplayColAsMenu() { + super(displayName); + initMenu(); + } + + final void initMenu() { + + int selCol = outline.getSelectedColumn(); + if (selCol < 0 ) { + selCol = 1; + } + + TableColumnModel columnModel = outline.getColumnModel(); + TableColumn column = columnModel.getColumn(selCol); + + JMenuItem parseAsEpochItem = new JMenuItem(Bundle.SQLiteTableView_DisplayAsMenuItem_Date()); + parseAsEpochItem.addActionListener((ActionEvent evt) -> { + column.setCellRenderer(new EpochTimeCellRenderer(true)); + }); + parseAsEpochItem.setEnabled(false); + add(parseAsEpochItem); + + JMenuItem parseAsOriginalItem = new JMenuItem(Bundle.SQLiteTableView_DisplayAsMenuItem_RawData()); + parseAsOriginalItem.addActionListener((ActionEvent evt) -> { + column.setCellRenderer(new EpochTimeCellRenderer(false)); + }); + parseAsOriginalItem.setEnabled(false); + add(parseAsOriginalItem); + + // Enable the relevant menuitem based on the current display state of the column + TableCellRenderer currRenderer = column.getCellRenderer(); + if (currRenderer instanceof EpochTimeCellRenderer) { + if (((EpochTimeCellRenderer) currRenderer).isRenderingAsEpoch()) { + parseAsOriginalItem.setEnabled(true); + } else { + parseAsEpochItem.setEnabled(true); + } + } + else { + parseAsEpochItem.setEnabled(true); + } + } + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/SQLiteViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/SQLiteViewer.java index 627d30e87c..49d6231435 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/SQLiteViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/SQLiteViewer.java @@ -42,10 +42,18 @@ import javax.swing.JComboBox; import javax.swing.SwingWorker; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.services.FileManager; +import org.sleuthkit.autopsy.casemodule.services.Services; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.ContentUtils; import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; +/** + * A file content viewer for SQLITE db files. + * + */ public class SQLiteViewer extends javax.swing.JPanel implements FileTypeViewer { public static final String[] SUPPORTED_MIMETYPES = new String[]{"application/x-sqlite3"}; @@ -62,8 +70,8 @@ public class SQLiteViewer extends javax.swing.JPanel implements FileTypeViewer { private int currPage = 0; // curr page of rows being displayed SQLiteTableView selectedTableView = new SQLiteTableView(); - private SwingWorker worker; + /** * Creates new form SQLiteViewer @@ -308,21 +316,25 @@ public class SQLiteViewer extends javax.swing.JPanel implements FileTypeViewer { private void processSQLiteFile(AbstractFile sqliteFile) { tablesDropdownList.removeAllItems(); - + new SwingWorker() { @Override protected Boolean doInBackground() throws Exception { try { // Copy the file to temp folder - tmpDBPathName = Case.getCurrentCase().getTempDirectory() + File.separator + sqliteFile.getName() + "-" + sqliteFile.getId(); + tmpDBPathName = Case.getCurrentCase().getTempDirectory() + File.separator + sqliteFile.getName(); tmpDBFile = new File(tmpDBPathName); ContentUtils.writeToFile(sqliteFile, tmpDBFile); + // look for any meta files associated with this DB - WAL, SHM + findAndCopySQLiteMetaFile(sqliteFile, sqliteFile.getName() + "-wal"); + findAndCopySQLiteMetaFile(sqliteFile, sqliteFile.getName() + "-shm"); + // Open copy using JDBC Class.forName("org.sqlite.JDBC"); //NON-NLS //load JDBC driver connection = DriverManager.getConnection("jdbc:sqlite:" + tmpDBPathName); //NON-NLS - + // Read all table names and schema return getTables(); } catch (IOException ex) { @@ -357,6 +369,45 @@ public class SQLiteViewer extends javax.swing.JPanel implements FileTypeViewer { } + /** + * Searches for a meta file associated with the give SQLite db + * If found, copies the file to the temp folder + * + * @param sqliteFile - SQLIte db file being processed + * @param metaFileName name of meta file to look for + * + * @return true if the meta file is found and copied successfully, false otherwise + */ + private boolean findAndCopySQLiteMetaFile(AbstractFile sqliteFile, String metaFileName ) { + + SleuthkitCase sleuthkitCase = Case.getCurrentCase().getSleuthkitCase(); + Services services = new Services(sleuthkitCase); + FileManager fileManager = services.getFileManager(); + + List metaFiles = null; + try { + metaFiles = fileManager.findFiles(sqliteFile.getDataSource(), metaFileName, sqliteFile.getParent().getName() ); + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Unexpected exception while searching SQLite meta file = " + metaFileName , ex); //NON-NLS + return false; + } + + if (metaFiles != null) { + for (AbstractFile metaFile: metaFiles) { + String tmpMetafilePathName = Case.getCurrentCase().getTempDirectory() + File.separator + metaFile.getName(); + + File tmpMetafile = new File(tmpMetafilePathName); + try { + ContentUtils.writeToFile(metaFile, tmpMetafile); + } catch (IOException ex) { + LOGGER.log(Level.SEVERE, "Unexpected exception while copying SQLite meta file = " + metaFileName , ex); //NON-NLS + return false; + } + } + } + + return true; + } /** * Gets the table names and their schema from loaded SQLite db file * diff --git a/Core/src/org/sleuthkit/autopsy/images/key-16.png b/Core/src/org/sleuthkit/autopsy/images/key-16.png new file mode 100644 index 0000000000..0e079240a6 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/key-16.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/keychain-16.png b/Core/src/org/sleuthkit/autopsy/images/keychain-16.png new file mode 100644 index 0000000000..348e7c5ce7 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/keychain-16.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/keys-dict-16.png b/Core/src/org/sleuthkit/autopsy/images/keys-dict-16.png new file mode 100644 index 0000000000..5304edac17 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/keys-dict-16.png differ diff --git a/nbproject/project.properties b/nbproject/project.properties index 0cb7b69b5e..781ec2c83b 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -6,8 +6,8 @@ app.name=${branding.token} ### if left unset, version will default to today's date app.version=4.6.0 ### build.type must be one of: DEVELOPMENT, RELEASE -build.type=RELEASE -#build.type=DEVELOPMENT +#build.type=RELEASE +build.type=DEVELOPMENT project.org.netbeans.progress=org-netbeans-api-progress project.org.sleuthkit.autopsy.experimental=Experimental