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;
105 class ListTimeline
extends BorderPane {
107 private static final Logger logger = Logger.getLogger(ListTimeline.class.getName());
109 private static final Image HASH_HIT =
new Image(
"/org/sleuthkit/autopsy/images/hashset_hits.png");
110 private static final Image TAG =
new Image(
"/org/sleuthkit/autopsy/images/green-tag-icon-16.png");
111 private static final Image FIRST =
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_first.png");
112 private static final Image PREVIOUS =
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_previous.png");
113 private static final Image NEXT =
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_next.png");
114 private static final Image LAST =
new Image(
"/org/sleuthkit/autopsy/timeline/images/resultset_last.png");
119 private static final Callback<TableColumn.CellDataFeatures<CombinedEvent, CombinedEvent>, ObservableValue<CombinedEvent>> CELL_VALUE_FACTORY = param ->
new SimpleObjectProperty<>(param.getValue());
121 private static final List<ChronoField> SCROLL_BY_UNITS = Arrays.asList(
123 ChronoField.MONTH_OF_YEAR,
124 ChronoField.DAY_OF_MONTH,
125 ChronoField.HOUR_OF_DAY,
126 ChronoField.MINUTE_OF_HOUR,
127 ChronoField.SECOND_OF_MINUTE);
129 private static final int DEFAULT_ROW_HEIGHT = 24;
132 private HBox navControls;
134 private ComboBox<ChronoField> scrollInrementComboBox;
136 private Button firstButton;
138 private Button previousButton;
140 private Button nextButton;
142 private Button lastButton;
144 private Label eventCountLabel;
146 private TableView<CombinedEvent> table;
148 private TableColumn<CombinedEvent, CombinedEvent> idColumn;
150 private TableColumn<CombinedEvent, CombinedEvent> dateTimeColumn;
152 private TableColumn<CombinedEvent, CombinedEvent> descriptionColumn;
154 private TableColumn<CombinedEvent, CombinedEvent> typeColumn;
156 private TableColumn<CombinedEvent, CombinedEvent> taggedColumn;
158 private TableColumn<CombinedEvent, CombinedEvent> hashHitColumn;
164 private final SortedSet<CombinedEvent> visibleEvents;
166 private final TimeLineController controller;
167 private final SleuthkitCase sleuthkitCase;
168 private final TagsManager tagsManager;
175 private final ListChangeListener<CombinedEvent> selectedEventListener =
new ListChangeListener<CombinedEvent>() {
177 public void onChanged(ListChangeListener.Change<? extends CombinedEvent> c) {
179 controller.selectEventIDs(table.getSelectionModel().getSelectedItems().stream()
180 .filter(Objects::nonNull)
182 .collect(Collectors.toSet()));
183 }
catch (TskCoreException ex) {
184 logger.log(Level.SEVERE,
"Error selecting events.", ex);
185 Notifications.create().owner(getScene().getWindow())
186 .text(
"Error selecting events.").showError();
196 ListTimeline(TimeLineController controller) {
197 this.controller = controller;
198 sleuthkitCase = controller.getAutopsyCase().getSleuthkitCase();
199 tagsManager = controller.getAutopsyCase().getServices().getTagsManager();
200 FXMLConstructor.construct(
this, ListTimeline.class,
"ListTimeline.fxml");
206 "# {0} - the number of events",
207 "ListTimeline.eventCountLabel.text={0} events"})
209 assert eventCountLabel != null :
"fx:id=\"eventCountLabel\" was not injected: check your FXML file 'ListViewPane.fxml'.";
210 assert table != null :
"fx:id=\"table\" was not injected: check your FXML file 'ListViewPane.fxml'.";
211 assert idColumn != null :
"fx:id=\"idColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
212 assert dateTimeColumn != null :
"fx:id=\"dateTimeColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
213 assert descriptionColumn != null :
"fx:id=\"descriptionColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
214 assert typeColumn != null :
"fx:id=\"typeColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
217 scrollInrementComboBox.setButtonCell(
new ChronoFieldListCell());
218 scrollInrementComboBox.setCellFactory(comboBox ->
new ChronoFieldListCell());
219 scrollInrementComboBox.getItems().setAll(SCROLL_BY_UNITS);
220 scrollInrementComboBox.getSelectionModel().select(ChronoField.YEAR);
221 ActionUtils.configureButton(
new ScrollToFirst(), firstButton);
222 ActionUtils.configureButton(
new ScrollToPrevious(), previousButton);
223 ActionUtils.configureButton(
new ScrollToNext(), nextButton);
224 ActionUtils.configureButton(
new ScrollToLast(), lastButton);
227 table.setRowFactory(tableView ->
new EventRow());
230 table.getColumns().remove(idColumn);
233 dateTimeColumn.setCellValueFactory(CELL_VALUE_FACTORY);
234 dateTimeColumn.setCellFactory(col ->
new TextEventTableCell(singleEvent
235 -> TimeLineController.getZonedFormatter().print(singleEvent.getStartMillis())));
237 descriptionColumn.setCellValueFactory(CELL_VALUE_FACTORY);
238 descriptionColumn.setCellFactory(col ->
new TextEventTableCell(singleEvent
239 -> singleEvent.getDescription(TimelineEvent.DescriptionLevel.FULL)));
241 typeColumn.setCellValueFactory(CELL_VALUE_FACTORY);
242 typeColumn.setCellFactory(col ->
new EventTypeCell());
244 taggedColumn.setCellValueFactory(CELL_VALUE_FACTORY);
245 taggedColumn.setCellFactory(col ->
new TaggedCell());
247 hashHitColumn.setCellValueFactory(CELL_VALUE_FACTORY);
248 hashHitColumn.setCellFactory(col ->
new HashHitCell());
251 eventCountLabel.textProperty().bind(
new StringBinding() {
253 bind(table.getItems());
257 protected String computeValue() {
258 return Bundle.ListTimeline_eventCountLabel_text(table.getItems().size());
263 table.getSelectionModel().getSelectedItems().addListener(selectedEventListener);
264 table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
265 selectEvents(controller.getSelectedEventIDs());
273 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
274 void setCombinedEvents(Collection<CombinedEvent> events) {
275 table.getItems().setAll(events);
283 void selectEvents(Collection<Long> selectedEventIDs) {
284 if (selectedEventIDs.isEmpty()) {
286 table.getSelectionModel().clearSelection();
298 table.getSelectionModel().getSelectedItems().removeListener(selectedEventListener);
300 table.getSelectionModel().clearSelection();
302 table.getSelectionModel().getSelectedItems().addListener(selectedEventListener);
305 int[] selectedIndices = table.getItems().stream()
306 .filter(combinedEvent -> Collections.disjoint(combinedEvent.getEventIDs(), selectedEventIDs) ==
false)
307 .mapToInt(table.getItems()::indexOf)
311 if (selectedIndices.length > 0) {
312 Integer firstSelectedIndex = selectedIndices[0];
313 table.getSelectionModel().selectIndices(firstSelectedIndex, selectedIndices);
314 scrollTo(firstSelectedIndex);
315 table.requestFocus();
327 List<Node> getTimeNavigationControls() {
328 return Collections.singletonList(navControls);
338 private void scrollToAndFocus(Integer index) {
339 table.requestFocus();
341 table.getFocusModel().focus(index);
349 private void scrollTo(Integer index) {
350 if (visibleEvents.contains(table.getItems().get(index)) ==
false) {
351 table.scrollTo(DoubleMath.roundToInt(index - ((table.getHeight() / DEFAULT_ROW_HEIGHT)) / 2, RoundingMode.HALF_EVEN));
359 private abstract class EventTableCell extends TableCell<CombinedEvent, CombinedEvent> {
368 TimelineEvent getEvent() {
372 @NbBundle.Messages({
"EventTableCell.updateItem.errorMessage=Error getting event by id."})
374 protected void updateItem(CombinedEvent item,
boolean empty) {
375 super.updateItem(item, empty);
377 if (empty || item == null) {
383 }
catch (TskCoreException ex) {
384 Notifications.create().owner(getScene().getWindow())
385 .text(Bundle.EventTableCell_updateItem_errorMessage()).showError();
386 logger.log(Level.SEVERE,
"Error getting event by id.", ex);
398 "ListView.EventTypeCell.modifiedTooltip=File Modified ( M )",
399 "ListView.EventTypeCell.accessedTooltip=File Accessed ( A )",
400 "ListView.EventTypeCell.createdTooltip=File Created ( B, for Born )",
401 "ListView.EventTypeCell.changedTooltip=File Changed ( C )"
404 protected void updateItem(CombinedEvent item,
boolean empty) {
405 super.updateItem(item, empty);
407 if (empty || item == null) {
412 if (item.
getEventTypes().stream().allMatch(TimelineEventType.FILE_SYSTEM.getSubTypes()::contains)) {
413 String typeString =
"";
414 VBox toolTipVbox =
new VBox(5);
416 for (TimelineEventType type : TimelineEventType.FILE_SYSTEM.getSubTypes()) {
418 if (type.equals(FILE_MODIFIED)) {
420 toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_modifiedTooltip(),
421 new ImageView(getImagePath(type))));
422 }
else if (type.equals(FILE_ACCESSED)) {
424 toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_accessedTooltip(),
425 new ImageView(getImagePath(type))));
426 }
else if (type.equals(FILE_CREATED)) {
428 toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_createdTooltip(),
429 new ImageView(getImagePath(type))));
430 }
else if (type.equals(FILE_CHANGED)) {
432 toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_changedTooltip(),
433 new ImageView(getImagePath(type))));
440 setGraphic(
new ImageView(getImagePath(FILE_SYSTEM)));
441 Tooltip tooltip =
new Tooltip();
442 tooltip.setGraphic(toolTipVbox);
446 TimelineEventType eventType = Iterables.getOnlyElement(item.
getEventTypes());
447 setText(eventType.getDisplayName());
448 setGraphic(
new ImageView(getImagePath(eventType)));
449 setTooltip(
new Tooltip(eventType.getDisplayName()));
464 setAlignment(Pos.CENTER);
468 "ListTimeline.taggedTooltip.error=There was a problem getting the tag names for the selected event.",
470 "ListTimeline.taggedTooltip.text=Tags:\n{0}"})
472 protected void updateItem(CombinedEvent item,
boolean empty) {
473 super.updateItem(item, empty);
475 if (empty || item == null || (getEvent().isTagged() ==
false)) {
483 setGraphic(
new ImageView(TAG));
485 SortedSet<String> tagNames =
new TreeSet<>();
488 Content file = sleuthkitCase.getContentById(getEvent().getFileObjID());
489 tagsManager.getContentTagsByContent(file).stream()
490 .map(tag -> tag.getName().getDisplayName())
491 .forEach(tagNames::add);
493 }
catch (TskCoreException ex) {
494 logger.log(Level.SEVERE,
"Failed to lookup tags for obj id " + getEvent().getFileObjID(), ex);
495 Platform.runLater(() -> {
496 Notifications.create()
497 .owner(getScene().getWindow())
498 .text(Bundle.ListTimeline_taggedTooltip_error())
502 getEvent().getArtifactID().ifPresent(artifactID -> {
505 BlackboardArtifact artifact = sleuthkitCase.getBlackboardArtifact(artifactID);
506 tagsManager.getBlackboardArtifactTagsByArtifact(artifact).stream()
507 .map(tag -> tag.getName().getDisplayName())
508 .forEach(tagNames::add);
509 }
catch (TskCoreException ex) {
510 logger.log(Level.SEVERE,
"Failed to lookup tags for artifact id " + artifactID, ex);
511 Platform.runLater(() -> {
512 Notifications.create()
513 .owner(getScene().getWindow())
514 .text(Bundle.ListTimeline_taggedTooltip_error())
519 Tooltip tooltip =
new Tooltip(Bundle.ListTimeline_taggedTooltip_text(String.join(
"\n", tagNames)));
520 tooltip.setGraphic(
new ImageView(TAG));
536 setAlignment(Pos.CENTER);
540 "ListTimeline.hashHitTooltip.error=There was a problem getting the hash set names for the selected event.",
541 "# {0} - hash set names",
542 "ListTimeline.hashHitTooltip.text=Hash Sets:\n{0}"})
544 protected void updateItem(CombinedEvent item,
boolean empty) {
545 super.updateItem(item, empty);
547 if (empty || item == null || (getEvent().isHashHit() ==
false)) {
556 setGraphic(
new ImageView(HASH_HIT));
558 Set<String> hashSetNames =
new TreeSet<>(sleuthkitCase.getContentById(getEvent().getFileObjID()).getHashSetNames());
559 Tooltip tooltip =
new Tooltip(Bundle.ListTimeline_hashHitTooltip_text(String.join(
"\n", hashSetNames)));
560 tooltip.setGraphic(
new ImageView(HASH_HIT));
562 }
catch (TskCoreException ex) {
563 logger.log(Level.SEVERE,
"Failed to lookup hash set names for obj id " + getEvent().getFileObjID(), ex);
564 Platform.runLater(() -> {
565 Notifications.create()
566 .owner(getScene().getWindow())
567 .text(Bundle.ListTimeline_hashHitTooltip_error())
591 setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
592 setEllipsisString(
" ... ");
596 protected void updateItem(CombinedEvent item,
boolean empty) {
597 super.updateItem(item, empty);
598 if (empty || item == null) {
602 String text = textSupplier.apply(getEvent());
604 setTooltip(
new Tooltip(text));
612 private class EventRow extends TableRow<CombinedEvent> {
615 "ListChart.errorMsg=There was a problem getting the content for the selected event.",
616 "EventRow.updateItem.errorMessage=Error getting event by id."})
618 protected void updateItem(CombinedEvent item,
boolean empty) {
619 CombinedEvent oldItem = getItem();
620 if (oldItem != null) {
621 visibleEvents.remove(oldItem);
623 super.updateItem(item, empty);
625 if (empty || item == null) {
626 setOnContextMenuRequested(ListTimeline::NOOPConsumer);
628 visibleEvents.add(item);
629 setOnContextMenuRequested(contextMenuEvent -> {
633 List<MenuItem> menuItems =
new ArrayList<>();
636 for (Action action : node.
getActions(
false)) {
637 if (action == null) {
639 menuItems.add(
new SeparatorMenuItem());
641 String actionName = Objects.toString(action.getValue(Action.NAME));
643 if (Arrays.asList(
"&Properties",
"Tools").contains(actionName) ==
false) {
644 if (action instanceof Presenter.Popup) {
651 JMenuItem submenu = ((Presenter.Popup) action).getPopupPresenter();
652 menuItems.add(SwingFXMenuUtils.createFXMenu(submenu));
654 menuItems.add(SwingFXMenuUtils.createFXMenu(
new Actions.MenuItem(action,
false)));
661 new ContextMenu(menuItems.toArray(
new MenuItem[menuItems.size()]))
662 .show(
this, contextMenuEvent.getScreenX(), contextMenuEvent.getScreenY());
663 }
catch (TskCoreException ex) {
664 logger.log(Level.SEVERE,
"Failed to lookup Sleuthkit object backing a TimelineEvent.", ex);
665 Platform.runLater(() -> {
666 Notifications.create()
667 .owner(getScene().getWindow())
668 .text(Bundle.ListChart_errorMsg())
677 public static <X>
void NOOPConsumer(X event) {
683 super(
"",
new Consumer<ActionEvent>() {
685 public void accept(ActionEvent actionEvent) {
689 setGraphic(
new ImageView(FIRST));
690 disabledProperty().bind(table.getFocusModel().focusedIndexProperty().lessThan(1));
697 super(
"",
new Consumer<ActionEvent>() {
699 public void accept(ActionEvent actionEvent) {
700 scrollToAndFocus(table.getItems().size() - 1);
703 setGraphic(
new ImageView(LAST));
704 IntegerBinding size = Bindings.size(table.getItems());
705 disabledProperty().bind(size.isEqualTo(0).or(
706 table.getFocusModel().focusedIndexProperty().greaterThanOrEqualTo(size.subtract(1))));
713 super(
"",
new Consumer<ActionEvent>() {
715 public void accept(ActionEvent actionEvent) {
716 ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem();
718 TemporalUnit selectedUnit = selectedChronoField.getBaseUnit();
720 int focusedIndex = table.getFocusModel().getFocusedIndex();
721 CombinedEvent focusedItem = table.getFocusModel().getFocusedItem();
722 if (-1 == focusedIndex || null == focusedItem) {
723 focusedItem = visibleEvents.first();
724 focusedIndex = table.getItems().indexOf(focusedItem);
727 ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.
getStartMillis()).atZone(timeZoneID);
728 ZonedDateTime nextDateTime = focusedDateTime.plus(1, selectedUnit);
729 for (ChronoField field : SCROLL_BY_UNITS) {
730 if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) {
731 nextDateTime = nextDateTime.with(field, field.rangeRefinedBy(nextDateTime).getMinimum());
734 long nextMillis = nextDateTime.toInstant().toEpochMilli();
736 int nextIndex = table.getItems().size() - 1;
737 for (
int i = focusedIndex; i < table.getItems().size(); i++) {
738 if (table.getItems().get(i).getStartMillis() >= nextMillis) {
743 scrollToAndFocus(nextIndex);
746 setGraphic(
new ImageView(NEXT));
747 IntegerBinding size = Bindings.size(table.getItems());
748 disabledProperty().bind(size.isEqualTo(0).or(
749 table.getFocusModel().focusedIndexProperty().greaterThanOrEqualTo(size.subtract(1))));
757 super(
"",
new Consumer<ActionEvent>() {
759 public void accept(ActionEvent actionEvent) {
761 ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem();
762 TemporalUnit selectedUnit = selectedChronoField.getBaseUnit();
764 int focusedIndex = table.getFocusModel().getFocusedIndex();
765 CombinedEvent focusedItem = table.getFocusModel().getFocusedItem();
766 if (-1 == focusedIndex || null == focusedItem) {
767 focusedItem = visibleEvents.last();
768 focusedIndex = table.getItems().indexOf(focusedItem);
771 ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.
getStartMillis()).atZone(timeZoneID);
772 ZonedDateTime previousDateTime = focusedDateTime.minus(1, selectedUnit);
774 for (ChronoField field : SCROLL_BY_UNITS) {
775 if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) {
776 previousDateTime = previousDateTime.with(field, field.rangeRefinedBy(previousDateTime).getMaximum());
779 long previousMillis = previousDateTime.toInstant().toEpochMilli();
781 int previousIndex = 0;
782 for (
int i = focusedIndex; i > 0; i--) {
783 if (table.getItems().get(i).getStartMillis() <= previousMillis) {
789 scrollToAndFocus(previousIndex);
792 setGraphic(
new ImageView(PREVIOUS));
793 disabledProperty().bind(table.getFocusModel().focusedIndexProperty().lessThan(1));
static EventNode createEventNode(final Long eventID, FilteredEventsModel eventsModel)
Action[] getActions(boolean context)
Long getRepresentativeEventID()
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)