19package org.sleuthkit.autopsy.timeline.ui.listvew;
21import com.google.common.collect.Iterables;
22import com.google.common.math.DoubleMath;
23import java.math.RoundingMode;
24import java.time.Instant;
25import java.time.ZoneId;
26import java.time.ZonedDateTime;
27import java.time.temporal.ChronoField;
28import java.time.temporal.TemporalUnit;
29import java.util.ArrayList;
30import java.util.Arrays;
31import java.util.Collection;
32import java.util.Collections;
33import java.util.Comparator;
35import java.util.Objects;
37import java.util.SortedSet;
38import java.util.TreeSet;
39import java.util.concurrent.ConcurrentSkipListSet;
40import java.util.function.Consumer;
41import java.util.function.Function;
42import java.util.logging.Level;
43import java.util.stream.Collectors;
44import javafx.application.Platform;
45import javafx.beans.binding.Bindings;
46import javafx.beans.binding.IntegerBinding;
47import javafx.beans.binding.StringBinding;
48import javafx.beans.property.SimpleObjectProperty;
49import javafx.beans.value.ObservableValue;
50import javafx.collections.ListChangeListener;
51import javafx.event.ActionEvent;
52import javafx.fxml.FXML;
53import javafx.geometry.Pos;
54import javafx.scene.Node;
55import javafx.scene.control.Button;
56import javafx.scene.control.ComboBox;
57import javafx.scene.control.ContextMenu;
58import javafx.scene.control.Label;
59import javafx.scene.control.MenuItem;
60import javafx.scene.control.OverrunStyle;
61import javafx.scene.control.SelectionMode;
62import javafx.scene.control.SeparatorMenuItem;
63import javafx.scene.control.TableCell;
64import javafx.scene.control.TableColumn;
65import javafx.scene.control.TableRow;
66import javafx.scene.control.TableView;
67import javafx.scene.control.Tooltip;
68import javafx.scene.image.Image;
69import javafx.scene.image.ImageView;
70import javafx.scene.layout.BorderPane;
71import javafx.scene.layout.HBox;
72import javafx.scene.layout.VBox;
73import javafx.util.Callback;
74import javax.swing.Action;
75import javax.swing.JMenuItem;
76import org.controlsfx.control.Notifications;
77import org.controlsfx.control.action.ActionUtils;
78import org.openide.awt.Actions;
79import org.openide.util.NbBundle;
80import org.openide.util.actions.Presenter;
81import org.sleuthkit.autopsy.casemodule.services.TagsManager;
82import org.sleuthkit.autopsy.coreutils.Logger;
83import org.sleuthkit.autopsy.coreutils.ThreadConfined;
84import org.sleuthkit.autopsy.timeline.ChronoFieldListCell;
85import org.sleuthkit.autopsy.timeline.FXMLConstructor;
86import org.sleuthkit.autopsy.timeline.TimeLineController;
87import org.sleuthkit.autopsy.timeline.explorernodes.EventNode;
88import static org.sleuthkit.autopsy.timeline.ui.EventTypeUtils.getImagePath;
89import org.sleuthkit.autopsy.timeline.ui.listvew.datamodel.CombinedEvent;
90import org.sleuthkit.datamodel.BlackboardArtifact;
91import org.sleuthkit.datamodel.Content;
92import org.sleuthkit.datamodel.SleuthkitCase;
93import org.sleuthkit.datamodel.TskCoreException;
94import org.sleuthkit.datamodel.TimelineEventType;
95import static org.sleuthkit.datamodel.TimelineEventType.FILE_ACCESSED;
96import static org.sleuthkit.datamodel.TimelineEventType.FILE_CHANGED;
97import static org.sleuthkit.datamodel.TimelineEventType.FILE_CREATED;
98import static org.sleuthkit.datamodel.TimelineEventType.FILE_MODIFIED;
99import static org.sleuthkit.datamodel.TimelineEventType.FILE_SYSTEM;
100import org.sleuthkit.datamodel.TimelineEvent;
101import org.sleuthkit.datamodel.TimelineLevelOfDetail;
106class 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 @SuppressWarnings(
"this-escape")
198 ListTimeline(TimeLineController controller) {
199 this.controller = controller;
200 sleuthkitCase = controller.getAutopsyCase().getSleuthkitCase();
201 tagsManager = controller.getAutopsyCase().getServices().getTagsManager();
202 FXMLConstructor.construct(
this, ListTimeline.class,
"ListTimeline.fxml");
208 "# {0} - the number of events",
209 "ListTimeline.eventCountLabel.text={0} events"})
211 assert eventCountLabel != null :
"fx:id=\"eventCountLabel\" was not injected: check your FXML file 'ListViewPane.fxml'.";
212 assert table != null :
"fx:id=\"table\" was not injected: check your FXML file 'ListViewPane.fxml'.";
213 assert idColumn != null :
"fx:id=\"idColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
214 assert dateTimeColumn != null :
"fx:id=\"dateTimeColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
215 assert descriptionColumn != null :
"fx:id=\"descriptionColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
216 assert typeColumn != null :
"fx:id=\"typeColumn\" was not injected: check your FXML file 'ListViewPane.fxml'.";
219 scrollInrementComboBox.setButtonCell(
new ChronoFieldListCell());
220 scrollInrementComboBox.setCellFactory(comboBox ->
new ChronoFieldListCell());
221 scrollInrementComboBox.getItems().setAll(SCROLL_BY_UNITS);
222 scrollInrementComboBox.getSelectionModel().select(ChronoField.YEAR);
223 ActionUtils.configureButton(
new ScrollToFirst(), firstButton);
225 ActionUtils.configureButton(
new ScrollToNext(), nextButton);
226 ActionUtils.configureButton(
new ScrollToLast(), lastButton);
229 table.setRowFactory(tableView ->
new EventRow());
232 table.getColumns().remove(idColumn);
235 dateTimeColumn.setCellValueFactory(CELL_VALUE_FACTORY);
237 -> TimeLineController.getZonedFormatter().print(singleEvent.getEventTimeInMs())));
239 descriptionColumn.setCellValueFactory(CELL_VALUE_FACTORY);
241 -> singleEvent.getDescription(TimelineLevelOfDetail.HIGH)));
243 typeColumn.setCellValueFactory(CELL_VALUE_FACTORY);
246 taggedColumn.setCellValueFactory(CELL_VALUE_FACTORY);
247 taggedColumn.setCellFactory(col ->
new TaggedCell());
249 hashHitColumn.setCellValueFactory(CELL_VALUE_FACTORY);
250 hashHitColumn.setCellFactory(col ->
new HashHitCell());
253 eventCountLabel.textProperty().bind(
new StringBinding() {
255 bind(table.getItems());
259 protected String computeValue() {
260 return Bundle.ListTimeline_eventCountLabel_text(table.getItems().size());
265 table.getSelectionModel().getSelectedItems().addListener(selectedEventListener);
266 table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
267 selectEvents(controller.getSelectedEventIDs());
275 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
276 void setCombinedEvents(Collection<CombinedEvent> events) {
277 table.getItems().setAll(events);
285 void selectEvents(Collection<Long> selectedEventIDs) {
286 if (selectedEventIDs.isEmpty()) {
288 table.getSelectionModel().clearSelection();
300 table.getSelectionModel().getSelectedItems().removeListener(selectedEventListener);
302 table.getSelectionModel().clearSelection();
304 table.getSelectionModel().getSelectedItems().addListener(selectedEventListener);
307 int[] selectedIndices = table.getItems().stream()
308 .filter(combinedEvent -> Collections.disjoint(combinedEvent.getEventIDs(), selectedEventIDs) ==
false)
309 .mapToInt(table.getItems()::indexOf)
313 if (selectedIndices.length > 0) {
314 Integer firstSelectedIndex = selectedIndices[0];
315 table.getSelectionModel().selectIndices(firstSelectedIndex, selectedIndices);
316 scrollTo(firstSelectedIndex);
317 table.requestFocus();
329 List<Node> getTimeNavigationControls() {
330 return Collections.singletonList(navControls);
340 private void scrollToAndFocus(Integer index) {
341 table.requestFocus();
343 table.getFocusModel().focus(index);
351 private void scrollTo(Integer index) {
352 if (visibleEvents.contains(table.getItems().get(index)) ==
false) {
353 table.scrollTo(DoubleMath.roundToInt(index - ((table.getHeight() / DEFAULT_ROW_HEIGHT)) / 2, RoundingMode.HALF_EVEN));
361 private abstract class EventTableCell extends TableCell<CombinedEvent, CombinedEvent> {
370 TimelineEvent getEvent() {
374 @NbBundle.Messages({
"EventTableCell.updateItem.errorMessage=Error getting event by id."})
377 super.updateItem(item, empty);
379 if (empty || item ==
null) {
385 }
catch (TskCoreException ex) {
386 Notifications.create().owner(getScene().getWindow())
387 .text(Bundle.EventTableCell_updateItem_errorMessage()).showError();
388 logger.log(Level.SEVERE,
"Error getting event by id.", ex);
400 "ListView.EventTypeCell.modifiedTooltip=File Modified ( M )",
401 "ListView.EventTypeCell.accessedTooltip=File Accessed ( A )",
402 "ListView.EventTypeCell.createdTooltip=File Created ( B, for Born )",
403 "ListView.EventTypeCell.changedTooltip=File Changed ( C )"
407 super.updateItem(item, empty);
409 if (empty || item ==
null) {
414 if (item.
getEventTypes().stream().allMatch(TimelineEventType.FILE_SYSTEM.getChildren()::contains)) {
415 String typeString =
"";
416 VBox toolTipVbox =
new VBox(5);
418 for (TimelineEventType type : TimelineEventType.FILE_SYSTEM.getChildren()) {
420 if (type.equals(FILE_MODIFIED)) {
422 toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_modifiedTooltip(),
423 new ImageView(getImagePath(type))));
424 }
else if (type.equals(FILE_ACCESSED)) {
426 toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_accessedTooltip(),
427 new ImageView(getImagePath(type))));
428 }
else if (type.equals(FILE_CREATED)) {
430 toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_createdTooltip(),
431 new ImageView(getImagePath(type))));
432 }
else if (type.equals(FILE_CHANGED)) {
434 toolTipVbox.getChildren().add(
new Label(Bundle.ListView_EventTypeCell_changedTooltip(),
435 new ImageView(getImagePath(type))));
442 setGraphic(
new ImageView(getImagePath(FILE_SYSTEM)));
443 Tooltip tooltip =
new Tooltip();
444 tooltip.setGraphic(toolTipVbox);
448 TimelineEventType eventType = Iterables.getOnlyElement(item.
getEventTypes());
449 setText(eventType.getDisplayName());
450 setGraphic(
new ImageView(getImagePath(eventType)));
451 setTooltip(
new Tooltip(eventType.getDisplayName()));
466 setAlignment(Pos.CENTER);
470 "ListTimeline.taggedTooltip.error=There was a problem getting the tag names for the selected event.",
472 "ListTimeline.taggedTooltip.text=Tags:\n{0}"})
475 super.updateItem(item, empty);
477 if (empty || item ==
null || (getEvent().eventSourceIsTagged() ==
false)) {
485 setGraphic(
new ImageView(TAG));
487 SortedSet<String> tagNames =
new TreeSet<>();
490 Content file = sleuthkitCase.getContentById(getEvent().getContentObjID());
491 tagsManager.getContentTagsByContent(file).stream()
492 .map(tag -> tag.getName().getDisplayName())
493 .forEach(tagNames::add);
495 }
catch (TskCoreException ex) {
496 logger.log(Level.SEVERE,
"Failed to lookup tags for obj id " + getEvent().getContentObjID(), ex);
497 Platform.runLater(() -> {
498 Notifications.create()
499 .owner(getScene().getWindow())
500 .text(Bundle.ListTimeline_taggedTooltip_error())
504 getEvent().getArtifactID().ifPresent(artifactID -> {
507 BlackboardArtifact artifact = sleuthkitCase.getBlackboardArtifact(artifactID);
508 tagsManager.getBlackboardArtifactTagsByArtifact(artifact).stream()
509 .map(tag -> tag.getName().getDisplayName())
510 .forEach(tagNames::add);
511 }
catch (TskCoreException ex) {
512 logger.log(Level.SEVERE,
"Failed to lookup tags for artifact id " + artifactID, ex);
513 Platform.runLater(() -> {
514 Notifications.create()
515 .owner(getScene().getWindow())
516 .text(Bundle.ListTimeline_taggedTooltip_error())
521 Tooltip tooltip =
new Tooltip(Bundle.ListTimeline_taggedTooltip_text(String.join(
"\n", tagNames)));
522 tooltip.setGraphic(
new ImageView(TAG));
538 setAlignment(Pos.CENTER);
542 "ListTimeline.hashHitTooltip.error=There was a problem getting the hash set names for the selected event.",
543 "# {0} - hash set names",
544 "ListTimeline.hashHitTooltip.text=Hash Sets:\n{0}"})
547 super.updateItem(item, empty);
549 if (empty || item ==
null || (getEvent().eventSourceHasHashHits()==
false)) {
558 setGraphic(
new ImageView(HASH_HIT));
560 Set<String> hashSetNames =
new TreeSet<>(sleuthkitCase.getContentById(getEvent().getContentObjID()).getHashSetNames());
561 Tooltip tooltip =
new Tooltip(Bundle.ListTimeline_hashHitTooltip_text(String.join(
"\n", hashSetNames)));
562 tooltip.setGraphic(
new ImageView(HASH_HIT));
564 }
catch (TskCoreException ex) {
565 logger.log(Level.SEVERE,
"Failed to lookup hash set names for obj id " + getEvent().getContentObjID(), ex);
566 Platform.runLater(() -> {
567 Notifications.create()
568 .owner(getScene().getWindow())
569 .text(Bundle.ListTimeline_hashHitTooltip_error())
591 TextEventTableCell(Function<TimelineEvent, String>
textSupplier) {
593 setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
594 setEllipsisString(
" ... ");
599 super.updateItem(item, empty);
600 if (empty || item ==
null) {
606 setTooltip(
new Tooltip(text));
614 private class EventRow extends TableRow<CombinedEvent> {
617 "ListChart.errorMsg=There was a problem getting the content for the selected event.",
618 "EventRow.updateItem.errorMessage=Error getting event by id."})
622 if (oldItem !=
null) {
623 visibleEvents.remove(oldItem);
625 super.updateItem(item, empty);
627 if (empty || item ==
null) {
628 setOnContextMenuRequested(ListTimeline::NOOPConsumer);
630 visibleEvents.add(item);
631 setOnContextMenuRequested(contextMenuEvent -> {
635 List<MenuItem> menuItems =
new ArrayList<>();
638 for (Action action : node.
getActions(
false)) {
639 if (action ==
null) {
641 menuItems.add(
new SeparatorMenuItem());
643 String actionName = Objects.toString(action.getValue(Action.NAME));
645 if (Arrays.asList(
"&Properties",
"Tools").contains(actionName) ==
false) {
646 if (action instanceof Presenter.Popup) {
653 JMenuItem submenu = ((Presenter.Popup) action).getPopupPresenter();
654 menuItems.add(SwingFXMenuUtils.createFXMenu(submenu));
656 menuItems.add(SwingFXMenuUtils.createFXMenu(
new Actions.MenuItem(action,
false)));
663 new ContextMenu(menuItems.toArray(
new MenuItem[menuItems.size()]))
664 .show(
this, contextMenuEvent.getScreenX(), contextMenuEvent.getScreenY());
665 }
catch (TskCoreException ex) {
666 logger.log(Level.SEVERE,
"Failed to lookup Sleuthkit object backing a TimelineEvent.", ex);
667 Platform.runLater(() -> {
668 Notifications.create()
669 .owner(getScene().getWindow())
670 .text(Bundle.ListChart_errorMsg())
679 public static <X>
void NOOPConsumer(X event) {
682 private class ScrollToFirst
extends org.controlsfx.control.action.Action {
685 super(
"",
new Consumer<ActionEvent>() {
687 public void accept(ActionEvent actionEvent) {
691 setGraphic(
new ImageView(FIRST));
692 disabledProperty().bind(table.getFocusModel().focusedIndexProperty().lessThan(1));
696 private class ScrollToLast
extends org.controlsfx.control.action.Action {
699 super(
"",
new Consumer<ActionEvent>() {
701 public void accept(ActionEvent actionEvent) {
702 scrollToAndFocus(table.getItems().size() - 1);
705 setGraphic(
new ImageView(LAST));
706 IntegerBinding size = Bindings.size(table.getItems());
707 disabledProperty().bind(size.isEqualTo(0).or(
708 table.getFocusModel().focusedIndexProperty().greaterThanOrEqualTo(size.subtract(1))));
712 private class ScrollToNext
extends org.controlsfx.control.action.Action {
715 super(
"",
new Consumer<ActionEvent>() {
717 public void accept(ActionEvent actionEvent) {
718 ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem();
720 TemporalUnit selectedUnit = selectedChronoField.getBaseUnit();
722 int focusedIndex = table.getFocusModel().getFocusedIndex();
723 CombinedEvent focusedItem = table.getFocusModel().getFocusedItem();
724 if (-1 == focusedIndex ||
null == focusedItem) {
725 focusedItem = visibleEvents.first();
726 focusedIndex = table.getItems().indexOf(focusedItem);
729 ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.
getStartMillis()).atZone(timeZoneID);
730 ZonedDateTime nextDateTime = focusedDateTime.plus(1, selectedUnit);
731 for (ChronoField field : SCROLL_BY_UNITS) {
732 if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) {
733 nextDateTime = nextDateTime.with(field, field.rangeRefinedBy(nextDateTime).getMinimum());
736 long nextMillis = nextDateTime.toInstant().toEpochMilli();
738 int nextIndex = table.getItems().size() - 1;
739 for (
int i = focusedIndex; i < table.getItems().size(); i++) {
740 if (table.getItems().get(i).getStartMillis() >= nextMillis) {
745 scrollToAndFocus(nextIndex);
748 setGraphic(
new ImageView(NEXT));
749 IntegerBinding size = Bindings.size(table.getItems());
750 disabledProperty().bind(size.isEqualTo(0).or(
751 table.getFocusModel().focusedIndexProperty().greaterThanOrEqualTo(size.subtract(1))));
756 private class ScrollToPrevious
extends org.controlsfx.control.action.Action {
759 super(
"",
new Consumer<ActionEvent>() {
761 public void accept(ActionEvent actionEvent) {
763 ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem();
764 TemporalUnit selectedUnit = selectedChronoField.getBaseUnit();
766 int focusedIndex = table.getFocusModel().getFocusedIndex();
767 CombinedEvent focusedItem = table.getFocusModel().getFocusedItem();
768 if (-1 == focusedIndex ||
null == focusedItem) {
769 focusedItem = visibleEvents.last();
770 focusedIndex = table.getItems().indexOf(focusedItem);
773 ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.
getStartMillis()).atZone(timeZoneID);
774 ZonedDateTime previousDateTime = focusedDateTime.minus(1, selectedUnit);
776 for (ChronoField field : SCROLL_BY_UNITS) {
777 if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) {
778 previousDateTime = previousDateTime.with(field, field.rangeRefinedBy(previousDateTime).getMaximum());
781 long previousMillis = previousDateTime.toInstant().toEpochMilli();
783 int previousIndex = 0;
784 for (
int i = focusedIndex; i > 0; i--) {
785 if (table.getItems().get(i).getStartMillis() <= previousMillis) {
791 scrollToAndFocus(previousIndex);
794 setGraphic(
new ImageView(PREVIOUS));
795 disabledProperty().bind(table.getFocusModel().focusedIndexProperty().lessThan(1));
static ZoneId getTimeZoneID()
Action[] getActions(boolean context)
static EventNode createEventNode(final Long eventID, EventsModel eventsModel)
void updateItem(CombinedEvent item, boolean empty)
void updateItem(CombinedEvent item, boolean empty)
void updateItem(CombinedEvent item, boolean empty)
void updateItem(CombinedEvent item, boolean empty)
void updateItem(CombinedEvent item, boolean empty)
final Function< TimelineEvent, String > textSupplier
void updateItem(CombinedEvent item, boolean empty)
Set< TimelineEventType > getEventTypes()
Long getRepresentativeEventID()