diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/TextUtil.java b/Core/src/org/sleuthkit/autopsy/coreutils/TextUtil.java index 053968d598..f2eed8e474 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/TextUtil.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/TextUtil.java @@ -57,4 +57,26 @@ public class TextUtil { return orientation; } + + + /** + * This method determines if a passed-in Java char (16 bits) is a valid + * UTF-8 printable character, returning true if so, false if not. + * + * Note that this method can have ramifications for characters outside the + * Unicode Base Multilingual Plane (BMP), which require more than 16 bits. + * We are using Java characters (16 bits) to look at the data and this will + * not accurately identify any non-BMP character (larger than 16 bits) + * ending with 0xFFFF and 0xFFFE. In the interest of a fast solution, we + * have chosen to ignore the extended planes above Unicode BMP for the time + * being. The net result of this is some non-BMP characters may be + * interspersed with '^' characters in Autopsy. + * + * @param ch the character to test + * + * @return Returns true if the character is valid UTF-8, false if not. + */ + public static boolean isValidSolrUTF8(char ch) { + return ((ch <= 0xFDD0 || ch >= 0xFDEF) && (ch > 0x1F || ch == 0x9 || ch == 0xA || ch == 0xD) && (ch != 0xFFFF) && (ch != 0xFFFE)); + } } diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbCreateDatabaseDialog.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbCreateDatabaseDialog.java index 96f0c1b16e..1b60250a6b 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbCreateDatabaseDialog.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbCreateDatabaseDialog.java @@ -22,6 +22,7 @@ import java.awt.Dimension; import java.awt.Toolkit; import java.io.File; import java.io.IOException; +import java.nio.file.Paths; import java.util.logging.Level; import javax.swing.JFileChooser; import javax.swing.JFrame; @@ -30,6 +31,7 @@ import org.apache.commons.io.FilenameUtils; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ModuleSettings; +import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.modules.hashdatabase.HashDbManager.HashDb; import org.sleuthkit.autopsy.modules.hashdatabase.HashDbManager.HashDb.KnownFilesType; import org.sleuthkit.autopsy.modules.hashdatabase.HashDbManager.HashDbManagerException; @@ -273,12 +275,17 @@ final class HashDbCreateDatabaseDialog extends javax.swing.JDialog { private void saveAsButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveAsButtonActionPerformed try { - String lastBaseDirectory = ""; + String lastBaseDirectory = Paths.get(PlatformUtil.getUserConfigDirectory(), "HashDatabases").toString(); if (ModuleSettings.settingExists(ModuleSettings.MAIN_SETTINGS, LAST_FILE_PATH_KEY)) { lastBaseDirectory = ModuleSettings.getConfigSetting(ModuleSettings.MAIN_SETTINGS, LAST_FILE_PATH_KEY); } StringBuilder path = new StringBuilder(); path.append(lastBaseDirectory); + File hashDbFolder = new File(path.toString()); + // create the folder if it doesn't exist + if (!hashDbFolder.exists()){ + hashDbFolder.mkdir(); + } if (!hashSetNameTextField.getText().isEmpty()) { path.append(File.separator).append(hashSetNameTextField.getText()); } else { diff --git a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties index 0be3a1924f..ba477498c3 100644 --- a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties @@ -179,7 +179,7 @@ ReportHTML.addThumbRows.dataType.msg=Tagged Results and Contents that contain im ReportHTML.thumbLink.tags=Tags\: ReportHTML.getName.text=Results - HTML ReportHTML.getDesc.text=A report about results and tagged items in HTML format. -ReportHTML.writeIndex.title=Autopsy Report for case {0} +ReportHTML.writeIndex.title=for case {0} ReportHTML.writeIndex.noFrames.msg=Your browser is not compatible with our frame setup. ReportHTML.writeIndex.noFrames.seeNav=Please see the navigation page for artifact links, ReportHTML.writeIndex.seeSum=and the summary page for a case summary. diff --git a/Core/src/org/sleuthkit/autopsy/report/ReportHTML.java b/Core/src/org/sleuthkit/autopsy/report/ReportHTML.java index eeca1efe56..5af71effae 100644 --- a/Core/src/org/sleuthkit/autopsy/report/ReportHTML.java +++ b/Core/src/org/sleuthkit/autopsy/report/ReportHTML.java @@ -838,7 +838,8 @@ class ReportHTML implements TableReportModule { try { indexOut = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(indexFilePath), "UTF-8")); //NON-NLS StringBuilder index = new StringBuilder(); - index.append("\n").append( //NON-NLS + final String reportTitle = reportBranding.getReportTitle(); + index.append("<head>\n<title>").append(reportTitle).append(" ").append( NbBundle.getMessage(this.getClass(), "ReportHTML.writeIndex.title", currentCase.getName())).append( "\n"); //NON-NLS index.append("\n"); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListTimeline.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListTimeline.java index 77fc831b29..efab1852c0 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListTimeline.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListTimeline.java @@ -23,9 +23,10 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.function.Function; import java.util.logging.Level; import javafx.application.Platform; -import javafx.beans.binding.Bindings; +import javafx.beans.binding.StringBinding; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; @@ -57,7 +58,7 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.datamodel.TskCoreException; /** - * The inner component that makes up the Lsit view. Manages the table. + * The inner component that makes up the List view. Manages the TableView. */ class ListTimeline extends BorderPane { @@ -100,6 +101,9 @@ class ListTimeline extends BorderPane { } @FXML + @NbBundle.Messages({ + "# {0} - the number of events", + "ListTimeline.evetnCountLabel.text={0} events"}) void initialize() { assert eventCountLabel != null : "fx:id=\"eventCountLabel\" was not injected: check your FXML file 'ListViewPane.fxml'."; assert table != null : "fx:id=\"table\" was not injected: check your FXML file 'ListViewPane.fxml'."; @@ -114,31 +118,44 @@ class ListTimeline extends BorderPane { //override default row with one that provides context menu.S table.setRowFactory(tableView -> new EventRow()); - //remove idColumn (can be used for debugging). + //remove idColumn (can be restored for debugging). table.getColumns().remove(idColumn); - // set up cell and cell-value factories for columns - // + ///// set up cell and cell-value factories for columns millisColumn.setCellValueFactory(CELL_VALUE_FACTORY); - millisColumn.setCellFactory(col -> new EpochMillisCell()); + millisColumn.setCellFactory(col -> new TextEventTableCell(singleEvent -> + TimeLineController.getZonedFormatter().print(singleEvent.getStartMillis()))); iconColumn.setCellValueFactory(CELL_VALUE_FACTORY); iconColumn.setCellFactory(col -> new ImageCell()); descriptionColumn.setCellValueFactory(CELL_VALUE_FACTORY); - descriptionColumn.setCellFactory(col -> new DescriptionCell()); + descriptionColumn.setCellFactory(col -> new TextEventTableCell(singleEvent -> + singleEvent.getDescription(DescriptionLoD.FULL))); baseTypeColumn.setCellValueFactory(CELL_VALUE_FACTORY); - baseTypeColumn.setCellFactory(col -> new BaseTypeCell()); + baseTypeColumn.setCellFactory(col -> new TextEventTableCell(singleEvent -> + singleEvent.getEventType().getBaseType().getDisplayName())); subTypeColumn.setCellValueFactory(CELL_VALUE_FACTORY); - subTypeColumn.setCellFactory(col -> new EventTypeCell()); + subTypeColumn.setCellFactory(col -> new TextEventTableCell(singleEvent -> + singleEvent.getEventType().getDisplayName())); knownColumn.setCellValueFactory(CELL_VALUE_FACTORY); - knownColumn.setCellFactory(col -> new KnownCell()); + knownColumn.setCellFactory(col -> new TextEventTableCell(singleEvent -> + singleEvent.getKnown().getName())); - //bind event count lable no number of items in table - eventCountLabel.textProperty().bind(Bindings.size(table.getItems()).asString().concat(" events")); + //bind event count label to number of items in the table + eventCountLabel.textProperty().bind(new StringBinding() { + { + bind(table.getItems()); + } + + @Override + protected String computeValue() { + return Bundle.ListTimeline_evetnCountLabel_text(table.getItems().size()); + } + }); } /** @@ -149,6 +166,15 @@ class ListTimeline extends BorderPane { table.getItems().clear(); } + /** + * Get the selected event ID. + * + * @return The selected event ID. + */ + Long getSelectedEventID() { + return table.getSelectionModel().getSelectedItem(); + } + /** * Set the Collection of events (by ID) to show in the table. * @@ -169,6 +195,18 @@ class ListTimeline extends BorderPane { return table.getSelectionModel().getSelectedItems(); } + /** + * Set the ID of the event that is selected. + * + * @param selectedEventID The ID of the event that should be selected. + */ + void selectEventID(Long selectedEventID) { + //restore selection. + table.scrollTo(selectedEventID); + table.getSelectionModel().select(selectedEventID); + table.requestFocus(); + } + /** * TableCell to show the icon for the type of an event. */ @@ -187,86 +225,23 @@ class ListTimeline extends BorderPane { } /** - * TableCell to show the full description for an event. + * TableCell to show text derived from a SingleEvent by the given Funtion. */ - private class DescriptionCell extends EventTableCell { + private class TextEventTableCell extends EventTableCell { - @Override - protected void updateItem(Long item, boolean empty) { - super.updateItem(item, empty); + private final Function textSupplier; - if (empty || item == null) { - setText(""); - } else { - setText(getEvent().getDescription(DescriptionLoD.FULL)); - } + TextEventTableCell(Function textSupplier) { + this.textSupplier = textSupplier; } - } - - /** - * TableCell to show the base type of an event. - */ - private class BaseTypeCell extends EventTableCell { @Override protected void updateItem(Long item, boolean empty) { super.updateItem(item, empty); - if (empty || item == null) { - setText(""); + setText(null); } else { - setText(getEvent().getEventType().getBaseType().getDisplayName()); - } - } - } - - /** - * TableCell to show the sub type of an event. - */ - private class EventTypeCell extends EventTableCell { - - @Override - protected void updateItem(Long item, boolean empty) { - super.updateItem(item, empty); - - if (empty || item == null) { - setText(""); - } else { - setText(getEvent().getEventType().getDisplayName()); - } - } - } - - /** - * TableCell to show the known state of the file backing an event. - */ - private class KnownCell extends EventTableCell { - - @Override - protected void updateItem(Long item, boolean empty) { - super.updateItem(item, empty); - - if (empty || item == null) { - setText(""); - } else { - setText(getEvent().getKnown().getName()); - } - } - } - - /** - * TableCell to show the (start) time of an event. - */ - private class EpochMillisCell extends EventTableCell { - - @Override - protected void updateItem(Long item, boolean empty) { - super.updateItem(item, empty); - - if (empty || item == null) { - setText(""); - } else { - setText(TimeLineController.getZonedFormatter().print(getEvent().getStartMillis())); + setText(textSupplier.apply(getEvent())); } } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListViewPane.java index 23278f0d86..91762abd7c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListViewPane.java @@ -29,23 +29,11 @@ import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.ui.AbstractTimeLineView; /** - * @param The type of data plotted along the x axis - * @param The type of data plotted along the y axis - * @param The type of nodes used to represent data items - * @param The type of the TimeLineChart this class uses to plot - * the data. Must extend Region. - * - * TODO: this is becoming (too?) closely tied to the notion that there is a - * XYChart doing the rendering. Is this a good idea? -jm - * - * TODO: pull up common history context menu items out of derived classes? -jm - * - * public abstract class AbstractVisualizationPane> extends BorderPane { + * An AbstractTimeLineView that uses a TableView to represent the events. */ public class ListViewPane extends AbstractTimeLineView { - private final ListTimeline listChart; + private final ListTimeline listTimeline; /** * Constructor @@ -54,15 +42,15 @@ public class ListViewPane extends AbstractTimeLineView { */ public ListViewPane(TimeLineController controller) { super(controller); - listChart = new ListTimeline(controller); + listTimeline = new ListTimeline(controller); //initialize chart; - setCenter(listChart); + setCenter(listTimeline); setSettingsNodes(new ListViewPane.ListViewSettingsPane().getChildrenUnmodifiable()); //keep controller's list of selected event IDs in sync with this list's - listChart.getSelectedEventIDs().addListener((Observable selectedIDs) -> { - controller.selectEventIDs(listChart.getSelectedEventIDs()); + listTimeline.getSelectedEventIDs().addListener((Observable selectedIDs) -> { + controller.selectEventIDs(listTimeline.getSelectedEventIDs()); }); } @@ -73,13 +61,10 @@ public class ListViewPane extends AbstractTimeLineView { @Override protected void clearData() { - listChart.clear(); + listTimeline.clear(); } private static class ListViewSettingsPane extends Parent { - - ListViewSettingsPane() { - } } private class ListUpdateTask extends ViewRefreshTask { @@ -90,21 +75,30 @@ public class ListViewPane extends AbstractTimeLineView { @Override protected Boolean call() throws Exception { - super.call(); //To change body of generated methods, choose Tools | Templates. + super.call(); if (isCancelled()) { return null; } + FilteredEventsModel eventsModel = getEventsModel(); - //clear the chart and set the horixontal axis + //grab the currently selected event + Long selectedEventID = listTimeline.getSelectedEventID(); + + //clear the chart and set the time range. resetView(eventsModel.getTimeRange()); - updateMessage("Querying db for events"); - //get the event stripes to be displayed + updateMessage("Querying DB for events"); + //get the IDs of th events to be displayed List eventIDs = eventsModel.getEventIDs(); - Platform.runLater(() -> listChart.setEventIDs(eventIDs)); + updateMessage("Updating UI"); + Platform.runLater(() -> { + //put the event IDs into the table. + listTimeline.setEventIDs(eventIDs); + //restore the selected event + listTimeline.selectEventID(selectedEventID); + }); - updateMessage("updating ui"); return eventIDs.isEmpty() == false; } @@ -117,6 +111,5 @@ public class ListViewPane extends AbstractTimeLineView { @Override protected void setDateValues(Interval timeRange) { } - } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java index 77568a8916..e7325f776c 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java @@ -31,6 +31,7 @@ import org.apache.solr.common.util.ContentStream; import org.apache.solr.common.SolrInputDocument; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.TextUtil; import org.sleuthkit.autopsy.datamodel.ContentUtils; import org.sleuthkit.datamodel.AbstractContent; import org.sleuthkit.datamodel.AbstractFile; @@ -298,6 +299,21 @@ class Ingester { String s = ""; try { s = new String(docChunkContentBuf, 0, read, docContentEncoding); + // Sanitize by replacing non-UTF-8 characters with caret '^' before adding to index + char[] chars = null; + for (int i = 0; i < s.length(); i++) { + if (!TextUtil.isValidSolrUTF8(s.charAt(i))) { + // only convert string to char[] if there is a non-UTF8 character + if (chars == null) { + chars = s.toCharArray(); + } + chars[i] = '^'; + } + } + // check if the string was modified (i.e. there was a non-UTF8 character found) + if (chars != null) { + s = new String(chars); + } } catch (UnsupportedEncodingException ex) { logger.log(Level.SEVERE, "Unsupported encoding", ex); //NON-NLS } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SolrSearchService.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SolrSearchService.java index 26c93f21e3..aa0bb5d830 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SolrSearchService.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SolrSearchService.java @@ -25,7 +25,6 @@ import org.apache.solr.client.solrj.impl.HttpSolrServer; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.TskCoreException; -import org.sleuthkit.autopsy.keywordsearch.Ingester.IngesterException; import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchService; import org.apache.solr.common.util.ContentStreamBase.StringStream; import org.openide.util.lookup.ServiceProvider; diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TikaTextExtractor.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TikaTextExtractor.java index 3ccf669b58..7022fb1b8b 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TikaTextExtractor.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TikaTextExtractor.java @@ -32,6 +32,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import org.sleuthkit.autopsy.coreutils.TextUtil; import java.util.concurrent.TimeoutException; import java.util.logging.Level; @@ -188,7 +189,7 @@ class TikaTextExtractor implements TextExtractor { // Sanitize by replacing non-UTF-8 characters with caret '^' for (int i = 0; i < totalRead; ++i) { - if (!isValidSolrUTF8(textChunkBuf[i])) { + if (!TextUtil.isValidSolrUTF8(textChunkBuf[i])) { textChunkBuf[i] = '^'; } } @@ -255,27 +256,6 @@ class TikaTextExtractor implements TextExtractor { return success; } - /** - * This method determines if a passed-in Java char (16 bits) is a valid - * UTF-8 printable character, returning true if so, false if not. - * - * Note that this method can have ramifications for characters outside the - * Unicode Base Multilingual Plane (BMP), which require more than 16 bits. - * We are using Java characters (16 bits) to look at the data and this will - * not accurately identify any non-BMP character (larger than 16 bits) - * ending with 0xFFFF and 0xFFFE. In the interest of a fast solution, we - * have chosen to ignore the extended planes above Unicode BMP for the time - * being. The net result of this is some non-BMP characters may be - * interspersed with '^' characters in Autopsy. - * - * @param ch the character to test - * - * @return Returns true if the character is valid UTF-8, false if not. - */ - private static boolean isValidSolrUTF8(char ch) { - return ((ch <= 0xFDD0 || ch >= 0xFDEF) && (ch > 0x1F || ch == 0x9 || ch == 0xA || ch == 0xD) && (ch != 0xFFFF) && (ch != 0xFFFE)); - } - @Override public boolean isContentTypeSpecific() { return true;