19 package org.sleuthkit.autopsy.timeline.ui.listvew;
21 import com.google.common.collect.Iterables;
22 import com.google.common.math.DoubleMath;
23 import java.math.RoundingMode;
24 import java.time.Instant;
25 import java.time.ZoneId;
26 import java.time.ZonedDateTime;
27 import java.time.temporal.ChronoField;
28 import java.time.temporal.TemporalUnit;
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.Comparator;
34 import java.util.List;
35 import java.util.Objects;
37 import java.util.SortedSet;
38 import java.util.TreeSet;
39 import java.util.concurrent.ConcurrentSkipListSet;
40 import java.util.function.Consumer;
41 import java.util.function.Function;
42 import java.util.logging.Level;
43 import java.util.stream.Collectors;
44 import javafx.application.Platform;
45 import javafx.beans.binding.Bindings;
46 import javafx.beans.binding.IntegerBinding;
47 import javafx.beans.binding.StringBinding;
48 import javafx.beans.property.SimpleObjectProperty;
49 import javafx.beans.value.ObservableValue;
50 import javafx.collections.ListChangeListener;
51 import javafx.event.ActionEvent;
52 import javafx.fxml.FXML;
53 import javafx.geometry.Pos;
54 import javafx.scene.Node;
55 import javafx.scene.control.Button;
56 import javafx.scene.control.ComboBox;
57 import javafx.scene.control.ContextMenu;
58 import javafx.scene.control.Label;
59 import javafx.scene.control.MenuItem;
60 import javafx.scene.control.OverrunStyle;
61 import javafx.scene.control.SelectionMode;
62 import javafx.scene.control.SeparatorMenuItem;
63 import javafx.scene.control.TableCell;
64 import javafx.scene.control.TableColumn;
65 import javafx.scene.control.TableRow;
66 import javafx.scene.control.TableView;
67 import javafx.scene.control.Tooltip;
68 import javafx.scene.image.Image;
69 import javafx.scene.image.ImageView;
70 import javafx.scene.layout.BorderPane;
71 import javafx.scene.layout.HBox;
72 import javafx.scene.layout.VBox;
73 import javafx.util.Callback;
74 import javax.swing.Action;
75 import javax.swing.JMenuItem;
76 import org.controlsfx.control.Notifications;
77 import org.controlsfx.control.action.ActionUtils;
78 import org.openide.awt.Actions;
79 import org.openide.util.NbBundle;
80 import org.openide.util.actions.Presenter;
95 import static org.
sleuthkit.datamodel.TimelineEventType.FILE_ACCESSED;
96 import static org.
sleuthkit.datamodel.TimelineEventType.FILE_CHANGED;
97 import static org.
sleuthkit.datamodel.TimelineEventType.FILE_CREATED;
98 import static org.
sleuthkit.datamodel.TimelineEventType.FILE_MODIFIED;
99 import static org.
sleuthkit.datamodel.TimelineEventType.FILE_SYSTEM;
106 class ListTimeline
extends BorderPane {
108 private static final Logger logger = Logger.getLogger(ListTimeline.class.getName());
110 private static final Image HASH_HIT =
new Image(
"/org/sleuthkit/autopsy/images/hashset_hits.png");
111 private static final Image TAG =
new Image(
"/org/sleuthkit/autopsy/images/green-tag-icon-16.png");
112 private static final Image FIRST =
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_first.png");
113 private static final Image PREVIOUS =
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_previous.png");
114 private static final Image NEXT =
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_next.png");
115 private static final Image LAST =
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_last.png");
120 private static final Callback<TableColumn.CellDataFeatures<CombinedEvent, CombinedEvent>, ObservableValue<CombinedEvent>> CELL_VALUE_FACTORY = param ->
new SimpleObjectProperty<>(param.getValue());
122 private static final List<ChronoField> SCROLL_BY_UNITS = Arrays.asList(
124 ChronoField.MONTH_OF_YEAR,
125 ChronoField.DAY_OF_MONTH,
126 ChronoField.HOUR_OF_DAY,
127 ChronoField.MINUTE_OF_HOUR,
128 ChronoField.SECOND_OF_MINUTE);
130 private static final int DEFAULT_ROW_HEIGHT = 24;
133 private HBox navControls;
135 private ComboBox<ChronoField> scrollInrementComboBox;
137 private Button firstButton;
139 private Button previousButton;
141 private Button nextButton;
143 private Button lastButton;
145 private Label eventCountLabel;
147 private TableView<CombinedEvent> table;
149 private TableColumn<CombinedEvent, CombinedEvent> idColumn;
151 private TableColumn<CombinedEvent, CombinedEvent> dateTimeColumn;
153 private TableColumn<CombinedEvent, CombinedEvent> descriptionColumn;
155 private TableColumn<CombinedEvent, CombinedEvent> typeColumn;
157 private TableColumn<CombinedEvent, CombinedEvent> taggedColumn;
159 private TableColumn<CombinedEvent, CombinedEvent> hashHitColumn;
165 private final SortedSet<CombinedEvent> visibleEvents;
167 private final TimeLineController controller;
168 private final SleuthkitCase sleuthkitCase;
169 private final TagsManager tagsManager;
176 private final ListChangeListener<CombinedEvent> selectedEventListener =
new ListChangeListener<CombinedEvent>() {
178 public void onChanged(ListChangeListener.Change<? extends CombinedEvent> c) {
180 controller.selectEventIDs(table.getSelectionModel().getSelectedItems().stream()
181 .filter(Objects::nonNull)
183 .collect(Collectors.toSet()));
184 }
catch (TskCoreException ex) {
185 logger.log(Level.SEVERE,
"Error selecting events.", ex);
186 Notifications.create().owner(getScene().getWindow())
187 .text(
"Error selecting events.").showError();
197 ListTimeline(TimeLineController controller) {
198 this.controller = controller;
199 sleuthkitCase = controller.getAutopsyCase().getSleuthkitCase();
200 tagsManager = controller.getAutopsyCase().getServices().getTagsManager();
201 FXMLConstructor.construct(
this, ListTimeline.class,
"ListTimeline.fxml");
207 "# {0} - the number of events",
208 "ListTimeline.eventCountLabel.text={0} events"})
210 assert eventCountLabel != null :
"fx:id=\"eventCountLabel\" was not injected: check your FXML file 'ListViewPane.fxml'.";
211 assert table != null :
"fx:id=\"table\" was not injected: check your FXML file 'ListViewPane.fxml'.";
212 assert idColumn != null :
"fx:id=\"idColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
213 assert dateTimeColumn != null :
"fx:id=\"dateTimeColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
214 assert descriptionColumn != null :
"fx:id=\"descriptionColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
215 assert typeColumn != null :
"fx:id=\"typeColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
218 scrollInrementComboBox.setButtonCell(
new ChronoFieldListCell());
219 scrollInrementComboBox.setCellFactory(comboBox ->
new ChronoFieldListCell());
220 scrollInrementComboBox.getItems().setAll(SCROLL_BY_UNITS);
221 scrollInrementComboBox.getSelectionModel().select(ChronoField.YEAR);
222 ActionUtils.configureButton(
new ScrollToFirst(), firstButton);
223 ActionUtils.configureButton(
new ScrollToPrevious(), previousButton);
224 ActionUtils.configureButton(
new ScrollToNext(), nextButton);
225 ActionUtils.configureButton(
new ScrollToLast(), lastButton);
228 table.setRowFactory(tableView ->
new EventRow());
231 table.getColumns().remove(idColumn);
234 dateTimeColumn.setCellValueFactory(CELL_VALUE_FACTORY);
235 dateTimeColumn.setCellFactory(col ->
new TextEventTableCell(singleEvent
236 -> TimeLineController.getZonedFormatter().print(singleEvent.getEventTimeInMs())));
238 descriptionColumn.setCellValueFactory(CELL_VALUE_FACTORY);
239 descriptionColumn.setCellFactory(col ->
new TextEventTableCell(singleEvent
240 -> singleEvent.getDescription(TimelineLevelOfDetail.HIGH)));
242 typeColumn.setCellValueFactory(CELL_VALUE_FACTORY);
243 typeColumn.setCellFactory(col ->
new EventTypeCell());
245 taggedColumn.setCellValueFactory(CELL_VALUE_FACTORY);
246 taggedColumn.setCellFactory(col ->
new TaggedCell());
248 hashHitColumn.setCellValueFactory(CELL_VALUE_FACTORY);
249 hashHitColumn.setCellFactory(col ->
new HashHitCell());
252 eventCountLabel.textProperty().bind(
new StringBinding() {
254 bind(table.getItems());
258 protected String computeValue() {
259 return Bundle.ListTimeline_eventCountLabel_text(table.getItems().size());
264 table.getSelectionModel().getSelectedItems().addListener(selectedEventListener);
265 table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
266 selectEvents(controller.getSelectedEventIDs());
274 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
275 void setCombinedEvents(Collection<CombinedEvent> events) {
276 table.getItems().setAll(events);
284 void selectEvents(Collection<Long> selectedEventIDs) {
285 if (selectedEventIDs.isEmpty()) {
287 table.getSelectionModel().clearSelection();
299 table.getSelectionModel().getSelectedItems().removeListener(selectedEventListener);
301 table.getSelectionModel().clearSelection();
303 table.getSelectionModel().getSelectedItems().addListener(selectedEventListener);
306 int[] selectedIndices = table.getItems().stream()
307 .filter(combinedEvent -> Collections.disjoint(combinedEvent.getEventIDs(), selectedEventIDs) ==
false)
308 .mapToInt(table.getItems()::indexOf)
312 if (selectedIndices.length > 0) {
313 Integer firstSelectedIndex = selectedIndices[0];
314 table.getSelectionModel().selectIndices(firstSelectedIndex, selectedIndices);
315 scrollTo(firstSelectedIndex);
316 table.requestFocus();
328 List<Node> getTimeNavigationControls() {
329 return Collections.singletonList(navControls);
339 private void scrollToAndFocus(Integer index) {
340 table.requestFocus();
342 table.getFocusModel().focus(index);
350 private void scrollTo(Integer index) {
351 if (visibleEvents.contains(table.getItems().get(index)) ==
false) {
352 table.scrollTo(DoubleMath.roundToInt(index - ((table.getHeight() / DEFAULT_ROW_HEIGHT)) / 2, RoundingMode.HALF_EVEN));
360 private abstract class EventTableCell extends TableCell<CombinedEvent, CombinedEvent> {
369 TimelineEvent getEvent() {
373 @NbBundle.Messages({
"EventTableCell.updateItem.errorMessage=Error getting event by id."})
375 protected void updateItem(CombinedEvent item,
boolean empty) {
376 super.updateItem(item, empty);
378 if (empty || item == null) {
384 }
catch (TskCoreException ex) {
385 Notifications.create().owner(getScene().getWindow())
386 .text(Bundle.EventTableCell_updateItem_errorMessage()).showError();
387 logger.log(Level.SEVERE,
"Error getting event by id.", ex);
399 "ListView.EventTypeCell.modifiedTooltip=File Modified ( M )",
400 "ListView.EventTypeCell.accessedTooltip=File Accessed ( A )",
401 "ListView.EventTypeCell.createdTooltip=File Created ( B, for Born )",
402 "ListView.EventTypeCell.changedTooltip=File Changed ( C )"
405 protected void updateItem(CombinedEvent item,
boolean empty) {
406 super.updateItem(item, empty);
408 if (empty || item == null) {
413 if (item.
getEventTypes().stream().allMatch(TimelineEventType.FILE_SYSTEM.getChildren()::contains)) {
414 String typeString =
"";
415 VBox toolTipVbox =
new VBox(5);
417 for (TimelineEventType type : TimelineEventType.FILE_SYSTEM.getChildren()) {
419 if (type.equals(FILE_MODIFIED)) {
421 toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_modifiedTooltip(),
422 new ImageView(getImagePath(type))));
423 }
else if (type.equals(FILE_ACCESSED)) {
425 toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_accessedTooltip(),
426 new ImageView(getImagePath(type))));
427 }
else if (type.equals(FILE_CREATED)) {
429 toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_createdTooltip(),
430 new ImageView(getImagePath(type))));
431 }
else if (type.equals(FILE_CHANGED)) {
433 toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_changedTooltip(),
434 new ImageView(getImagePath(type))));
441 setGraphic(
new ImageView(getImagePath(FILE_SYSTEM)));
442 Tooltip tooltip =
new Tooltip();
443 tooltip.setGraphic(toolTipVbox);
447 TimelineEventType eventType = Iterables.getOnlyElement(item.
getEventTypes());
448 setText(eventType.getDisplayName());
449 setGraphic(
new ImageView(getImagePath(eventType)));
450 setTooltip(
new Tooltip(eventType.getDisplayName()));
465 setAlignment(Pos.CENTER);
469 "ListTimeline.taggedTooltip.error=There was a problem getting the tag names for the selected event.",
471 "ListTimeline.taggedTooltip.text=Tags:\n{0}"})
473 protected void updateItem(CombinedEvent item,
boolean empty) {
474 super.updateItem(item, empty);
476 if (empty || item == null || (getEvent().eventSourceIsTagged() ==
false)) {
484 setGraphic(
new ImageView(TAG));
486 SortedSet<String> tagNames =
new TreeSet<>();
489 Content file = sleuthkitCase.getContentById(getEvent().getContentObjID());
490 tagsManager.getContentTagsByContent(file).stream()
491 .map(tag -> tag.getName().getDisplayName())
492 .forEach(tagNames::add);
494 }
catch (TskCoreException ex) {
495 logger.log(Level.SEVERE,
"Failed to lookup tags for obj id " + getEvent().getContentObjID(), ex);
496 Platform.runLater(() -> {
497 Notifications.create()
498 .owner(getScene().getWindow())
499 .text(Bundle.ListTimeline_taggedTooltip_error())
503 getEvent().getArtifactID().ifPresent(artifactID -> {
506 BlackboardArtifact artifact = sleuthkitCase.getBlackboardArtifact(artifactID);
507 tagsManager.getBlackboardArtifactTagsByArtifact(artifact).stream()
508 .map(tag -> tag.getName().getDisplayName())
509 .forEach(tagNames::add);
510 }
catch (TskCoreException ex) {
511 logger.log(Level.SEVERE,
"Failed to lookup tags for artifact id " + artifactID, ex);
512 Platform.runLater(() -> {
513 Notifications.create()
514 .owner(getScene().getWindow())
515 .text(Bundle.ListTimeline_taggedTooltip_error())
520 Tooltip tooltip =
new Tooltip(Bundle.ListTimeline_taggedTooltip_text(String.join(
"\n", tagNames)));
521 tooltip.setGraphic(
new ImageView(TAG));
537 setAlignment(Pos.CENTER);
541 "ListTimeline.hashHitTooltip.error=There was a problem getting the hash set names for the selected event.",
542 "# {0} - hash set names",
543 "ListTimeline.hashHitTooltip.text=Hash Sets:\n{0}"})
545 protected void updateItem(CombinedEvent item,
boolean empty) {
546 super.updateItem(item, empty);
548 if (empty || item == null || (getEvent().eventSourceHasHashHits()==
false)) {
557 setGraphic(
new ImageView(HASH_HIT));
559 Set<String> hashSetNames =
new TreeSet<>(sleuthkitCase.getContentById(getEvent().getContentObjID()).getHashSetNames());
560 Tooltip tooltip =
new Tooltip(Bundle.ListTimeline_hashHitTooltip_text(String.join(
"\n", hashSetNames)));
561 tooltip.setGraphic(
new ImageView(HASH_HIT));
563 }
catch (TskCoreException ex) {
564 logger.log(Level.SEVERE,
"Failed to lookup hash set names for obj id " + getEvent().getContentObjID(), ex);
565 Platform.runLater(() -> {
566 Notifications.create()
567 .owner(getScene().getWindow())
568 .text(Bundle.ListTimeline_hashHitTooltip_error())
592 setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
593 setEllipsisString(
" ... ");
597 protected void updateItem(CombinedEvent item,
boolean empty) {
598 super.updateItem(item, empty);
599 if (empty || item == null) {
603 String text = textSupplier.apply(getEvent());
605 setTooltip(
new Tooltip(text));
613 private class EventRow extends TableRow<CombinedEvent> {
616 "ListChart.errorMsg=There was a problem getting the content for the selected event.",
617 "EventRow.updateItem.errorMessage=Error getting event by id."})
619 protected void updateItem(CombinedEvent item,
boolean empty) {
620 CombinedEvent oldItem = getItem();
621 if (oldItem != null) {
622 visibleEvents.remove(oldItem);
624 super.updateItem(item, empty);
626 if (empty || item == null) {
627 setOnContextMenuRequested(ListTimeline::NOOPConsumer);
629 visibleEvents.add(item);
630 setOnContextMenuRequested(contextMenuEvent -> {
634 List<MenuItem> menuItems =
new ArrayList<>();
637 for (Action action : node.
getActions(
false)) {
638 if (action == null) {
640 menuItems.add(
new SeparatorMenuItem());
642 String actionName = Objects.toString(action.getValue(Action.NAME));
644 if (Arrays.asList(
"&Properties",
"Tools").contains(actionName) ==
false) {
645 if (action instanceof Presenter.Popup) {
652 JMenuItem submenu = ((Presenter.Popup) action).getPopupPresenter();
653 menuItems.add(SwingFXMenuUtils.createFXMenu(submenu));
655 menuItems.add(SwingFXMenuUtils.createFXMenu(
new Actions.MenuItem(action,
false)));
662 new ContextMenu(menuItems.toArray(
new MenuItem[menuItems.size()]))
663 .show(
this, contextMenuEvent.getScreenX(), contextMenuEvent.getScreenY());
664 }
catch (TskCoreException ex) {
665 logger.log(Level.SEVERE,
"Failed to lookup Sleuthkit object backing a TimelineEvent.", ex);
666 Platform.runLater(() -> {
667 Notifications.create()
668 .owner(getScene().getWindow())
669 .text(Bundle.ListChart_errorMsg())
678 public static <X>
void NOOPConsumer(X event) {
684 super(
"",
new Consumer<ActionEvent>() {
686 public void accept(ActionEvent actionEvent) {
690 setGraphic(
new ImageView(FIRST));
691 disabledProperty().bind(table.getFocusModel().focusedIndexProperty().lessThan(1));
698 super(
"",
new Consumer<ActionEvent>() {
700 public void accept(ActionEvent actionEvent) {
701 scrollToAndFocus(table.getItems().size() - 1);
704 setGraphic(
new ImageView(LAST));
705 IntegerBinding size = Bindings.size(table.getItems());
706 disabledProperty().bind(size.isEqualTo(0).or(
707 table.getFocusModel().focusedIndexProperty().greaterThanOrEqualTo(size.subtract(1))));
714 super(
"",
new Consumer<ActionEvent>() {
716 public void accept(ActionEvent actionEvent) {
717 ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem();
719 TemporalUnit selectedUnit = selectedChronoField.getBaseUnit();
721 int focusedIndex = table.getFocusModel().getFocusedIndex();
722 CombinedEvent focusedItem = table.getFocusModel().getFocusedItem();
723 if (-1 == focusedIndex || null == focusedItem) {
724 focusedItem = visibleEvents.first();
725 focusedIndex = table.getItems().indexOf(focusedItem);
728 ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.
getStartMillis()).atZone(timeZoneID);
729 ZonedDateTime nextDateTime = focusedDateTime.plus(1, selectedUnit);
730 for (ChronoField field : SCROLL_BY_UNITS) {
731 if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) {
732 nextDateTime = nextDateTime.with(field, field.rangeRefinedBy(nextDateTime).getMinimum());
735 long nextMillis = nextDateTime.toInstant().toEpochMilli();
737 int nextIndex = table.getItems().size() - 1;
738 for (
int i = focusedIndex; i < table.getItems().size(); i++) {
739 if (table.getItems().get(i).getStartMillis() >= nextMillis) {
744 scrollToAndFocus(nextIndex);
747 setGraphic(
new ImageView(NEXT));
748 IntegerBinding size = Bindings.size(table.getItems());
749 disabledProperty().bind(size.isEqualTo(0).or(
750 table.getFocusModel().focusedIndexProperty().greaterThanOrEqualTo(size.subtract(1))));
758 super(
"",
new Consumer<ActionEvent>() {
760 public void accept(ActionEvent actionEvent) {
762 ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem();
763 TemporalUnit selectedUnit = selectedChronoField.getBaseUnit();
765 int focusedIndex = table.getFocusModel().getFocusedIndex();
766 CombinedEvent focusedItem = table.getFocusModel().getFocusedItem();
767 if (-1 == focusedIndex || null == focusedItem) {
768 focusedItem = visibleEvents.last();
769 focusedIndex = table.getItems().indexOf(focusedItem);
772 ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.
getStartMillis()).atZone(timeZoneID);
773 ZonedDateTime previousDateTime = focusedDateTime.minus(1, selectedUnit);
775 for (ChronoField field : SCROLL_BY_UNITS) {
776 if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) {
777 previousDateTime = previousDateTime.with(field, field.rangeRefinedBy(previousDateTime).getMaximum());
780 long previousMillis = previousDateTime.toInstant().toEpochMilli();
782 int previousIndex = 0;
783 for (
int i = focusedIndex; i > 0; i--) {
784 if (table.getItems().get(i).getStartMillis() <= previousMillis) {
790 scrollToAndFocus(previousIndex);
793 setGraphic(
new ImageView(PREVIOUS));
794 disabledProperty().bind(table.getFocusModel().focusedIndexProperty().lessThan(1));
Action[] getActions(boolean context)
Long getRepresentativeEventID()
static EventNode createEventNode(final Long eventID, EventsModel eventsModel)
void updateItem(CombinedEvent item, boolean empty)
final Function< TimelineEvent, String > textSupplier
void updateItem(CombinedEvent item, boolean empty)
void updateItem(CombinedEvent item, boolean empty)
void updateItem(CombinedEvent item, boolean empty)
void updateItem(CombinedEvent item, boolean empty)
static ZoneId getTimeZoneID()
Set< TimelineEventType > getEventTypes()
static String getImagePath(TimelineEventType type)
void updateItem(CombinedEvent item, boolean empty)