Sleuth Kit Java Bindings (JNI)  4.11.1
Java bindings for using The Sleuth Kit
TimelineManager.java
Go to the documentation of this file.
1 /*
2  * Sleuth Kit Data Model
3  *
4  * Copyright 2018-2020 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.datamodel;
20 
21 import com.google.common.annotations.Beta;
22 import com.google.common.collect.ImmutableList;
23 import com.google.common.collect.ImmutableMap;
24 import java.sql.PreparedStatement;
25 import java.sql.ResultSet;
26 import java.sql.SQLException;
27 import java.sql.Statement;
28 import java.sql.Types;
29 import java.time.Instant;
30 import java.util.ArrayList;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.HashMap;
34 import java.util.HashSet;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Objects;
38 import java.util.Optional;
39 import java.util.Set;
40 import java.util.logging.Level;
41 import java.util.logging.Logger;
42 import java.util.stream.Collectors;
43 import java.util.stream.Stream;
44 import org.joda.time.DateTimeZone;
45 import org.joda.time.Interval;
48 import static org.sleuthkit.datamodel.CollectionUtils.isNotEmpty;
49 import static org.sleuthkit.datamodel.CommManagerSqlStringUtils.buildCSVString;
52 
56 public final class TimelineManager {
57 
58  private static final Logger logger = Logger.getLogger(TimelineManager.class.getName());
59 
63  private static final ImmutableList<TimelineEventType> ROOT_CATEGORY_AND_FILESYSTEM_TYPES
64  = ImmutableList.of(
73 
80  private static final ImmutableList<TimelineEventType> PREDEFINED_EVENT_TYPES
81  = new ImmutableList.Builder<TimelineEventType>()
84  .build();
85 
86  // all known artifact type ids (used for determining if an artifact is standard or custom event)
87  private static final Set<Integer> ARTIFACT_TYPE_IDS = Stream.of(BlackboardArtifact.ARTIFACT_TYPE.values())
88  .map(artType -> artType.getTypeID())
89  .collect(Collectors.toSet());
90 
91  private final SleuthkitCase caseDB;
92 
97  private static final Long MAX_TIMESTAMP_TO_ADD = Instant.now().getEpochSecond() + 394200000;
98 
102  private final Map<Long, TimelineEventType> eventTypeIDMap = new HashMap<>();
103 
114  this.caseDB = caseDB;
115 
116  List<TimelineEventType> fullList = new ArrayList<>();
117  fullList.addAll(ROOT_CATEGORY_AND_FILESYSTEM_TYPES);
118  fullList.addAll(PREDEFINED_EVENT_TYPES);
119 
121  try (final CaseDbConnection con = caseDB.getConnection();
122  final PreparedStatement pStatement = con.prepareStatement(
123  insertOrIgnore(" INTO tsk_event_types(event_type_id, display_name, super_type_id) VALUES (?, ?, ?)"),
124  Statement.NO_GENERATED_KEYS)) {
125  for (TimelineEventType type : fullList) {
126  pStatement.setLong(1, type.getTypeID());
127  pStatement.setString(2, escapeSingleQuotes(type.getDisplayName()));
128  if (type != type.getParent()) {
129  pStatement.setLong(3, type.getParent().getTypeID());
130  } else {
131  pStatement.setNull(3, java.sql.Types.INTEGER);
132  }
133 
134  con.executeUpdate(pStatement);
135  eventTypeIDMap.put(type.getTypeID(), type);
136  }
137  } catch (SQLException ex) {
138  throw new TskCoreException("Failed to initialize timeline event types", ex); // NON-NLS
139  } finally {
141  }
142  }
143 
155  public Interval getSpanningInterval(Collection<Long> eventIDs) throws TskCoreException {
156  if (eventIDs.isEmpty()) {
157  return null;
158  }
159  final String query = "SELECT Min(time) as minTime, Max(time) as maxTime FROM tsk_events WHERE event_id IN (" + buildCSVString(eventIDs) + ")"; //NON-NLS
161  try (CaseDbConnection con = caseDB.getConnection();
162  Statement stmt = con.createStatement();
163  ResultSet results = stmt.executeQuery(query);) {
164  if (results.next()) {
165  return new Interval(results.getLong("minTime") * 1000, (results.getLong("maxTime") + 1) * 1000, DateTimeZone.UTC); // NON-NLS
166  }
167  } catch (SQLException ex) {
168  throw new TskCoreException("Error executing get spanning interval query: " + query, ex); // NON-NLS
169  } finally {
171  }
172  return null;
173  }
174 
187  public Interval getSpanningInterval(Interval timeRange, TimelineFilter.RootFilter filter, DateTimeZone timeZone) throws TskCoreException {
188  long start = timeRange.getStartMillis() / 1000;
189  long end = timeRange.getEndMillis() / 1000;
190  String sqlWhere = getSQLWhere(filter);
191  String augmentedEventsTablesSQL = getAugmentedEventsTablesSQL(filter);
192  String queryString = " SELECT (SELECT Max(time) FROM " + augmentedEventsTablesSQL
193  + " WHERE time <=" + start + " AND " + sqlWhere + ") AS start,"
194  + " (SELECT Min(time) FROM " + augmentedEventsTablesSQL
195  + " WHERE time >= " + end + " AND " + sqlWhere + ") AS end";//NON-NLS
197  try (CaseDbConnection con = caseDB.getConnection();
198  Statement stmt = con.createStatement(); //can't use prepared statement because of complex where clause
199  ResultSet results = stmt.executeQuery(queryString);) {
200 
201  if (results.next()) {
202  long start2 = results.getLong("start"); // NON-NLS
203  long end2 = results.getLong("end"); // NON-NLS
204 
205  if (end2 == 0) {
206  end2 = getMaxEventTime();
207  }
208  return new Interval(start2 * 1000, (end2 + 1) * 1000, timeZone);
209  }
210  } catch (SQLException ex) {
211  throw new TskCoreException("Failed to get MIN time.", ex); // NON-NLS
212  } finally {
214  }
215  return null;
216  }
217 
227  public TimelineEvent getEventById(long eventID) throws TskCoreException {
228  String sql = "SELECT * FROM " + getAugmentedEventsTablesSQL(false) + " WHERE event_id = " + eventID;
230  try (CaseDbConnection con = caseDB.getConnection();
231  Statement stmt = con.createStatement();) {
232  try (ResultSet results = stmt.executeQuery(sql);) {
233  if (results.next()) {
234  int typeID = results.getInt("event_type_id");
235  TimelineEventType type = getEventType(typeID).orElseThrow(() -> newEventTypeMappingException(typeID)); //NON-NLS
236  return new TimelineEvent(eventID,
237  results.getLong("data_source_obj_id"),
238  results.getLong("content_obj_id"),
239  results.getLong("artifact_id"),
240  results.getLong("time"),
241  type, results.getString("full_description"),
242  results.getString("med_description"),
243  results.getString("short_description"),
244  intToBoolean(results.getInt("hash_hit")),
245  intToBoolean(results.getInt("tagged")));
246  }
247  }
248  } catch (SQLException sqlEx) {
249  throw new TskCoreException("Error while executing query " + sql, sqlEx); // NON-NLS
250  } finally {
252  }
253  return null;
254  }
255 
267  public List<Long> getEventIDs(Interval timeRange, TimelineFilter.RootFilter filter) throws TskCoreException {
268  Long startTime = timeRange.getStartMillis() / 1000;
269  Long endTime = timeRange.getEndMillis() / 1000;
270 
271  if (Objects.equals(startTime, endTime)) {
272  endTime++; //make sure end is at least 1 millisecond after start
273  }
274 
275  ArrayList<Long> resultIDs = new ArrayList<>();
276 
277  String query = "SELECT tsk_events.event_id AS event_id FROM " + getAugmentedEventsTablesSQL(filter)
278  + " WHERE time >= " + startTime + " AND time <" + endTime + " AND " + getSQLWhere(filter) + " ORDER BY time ASC"; // NON-NLS
280  try (CaseDbConnection con = caseDB.getConnection();
281  Statement stmt = con.createStatement();
282  ResultSet results = stmt.executeQuery(query);) {
283  while (results.next()) {
284  resultIDs.add(results.getLong("event_id")); //NON-NLS
285  }
286 
287  } catch (SQLException sqlEx) {
288  throw new TskCoreException("Error while executing query " + query, sqlEx); // NON-NLS
289  } finally {
291  }
292 
293  return resultIDs;
294  }
295 
304  public Long getMaxEventTime() throws TskCoreException {
306  try (CaseDbConnection con = caseDB.getConnection();
307  Statement stms = con.createStatement();
308  ResultSet results = stms.executeQuery(STATEMENTS.GET_MAX_TIME.getSQL());) {
309  if (results.next()) {
310  return results.getLong("max"); // NON-NLS
311  }
312  } catch (SQLException ex) {
313  throw new TskCoreException("Error while executing query " + STATEMENTS.GET_MAX_TIME.getSQL(), ex); // NON-NLS
314  } finally {
316  }
317  return -1l;
318  }
319 
328  public Long getMinEventTime() throws TskCoreException {
330  try (CaseDbConnection con = caseDB.getConnection();
331  Statement stms = con.createStatement();
332  ResultSet results = stms.executeQuery(STATEMENTS.GET_MIN_TIME.getSQL());) {
333  if (results.next()) {
334  return results.getLong("min"); // NON-NLS
335  }
336  } catch (SQLException ex) {
337  throw new TskCoreException("Error while executing query " + STATEMENTS.GET_MAX_TIME.getSQL(), ex); // NON-NLS
338  } finally {
340  }
341  return -1l;
342  }
343 
352  public Optional<TimelineEventType> getEventType(long eventTypeID) {
353  // The parent EventType with ID 22 has been deprecated. This ID had two
354  // children which have be reassigned to MISC_TYPES.
355  if(eventTypeID == TimelineEventType.DEPRECATED_OTHER_EVENT_ID) {
356  return Optional.of(TimelineEventType.MISC_TYPES);
357  }
358 
359  return Optional.ofNullable(eventTypeIDMap.get(eventTypeID));
360  }
361 
367  public ImmutableList<TimelineEventType> getEventTypes() {
368  return ImmutableList.copyOf(eventTypeIDMap.values());
369  }
370 
371  private String insertOrIgnore(String query) {
372  switch (caseDB.getDatabaseType()) {
373  case POSTGRESQL:
374  return " INSERT " + query + " ON CONFLICT DO NOTHING "; //NON-NLS
375  case SQLITE:
376  return " INSERT OR IGNORE " + query; //NON-NLS
377  default:
378  throw new UnsupportedOperationException("Unsupported DB type: " + caseDB.getDatabaseType().name());
379  }
380  }
381 
385  private enum STATEMENTS {
386 
387  GET_MAX_TIME("SELECT Max(time) AS max FROM tsk_events"), // NON-NLS
388  GET_MIN_TIME("SELECT Min(time) AS min FROM tsk_events"); // NON-NLS
389 
390  private final String sql;
391 
392  private STATEMENTS(String sql) {
393  this.sql = sql;
394  }
395 
396  String getSQL() {
397  return sql;
398  }
399  }
400 
411  public List<Long> getEventIDsForArtifact(BlackboardArtifact artifact) throws TskCoreException {
412  ArrayList<Long> eventIDs = new ArrayList<>();
413 
414  String query
415  = "SELECT event_id FROM tsk_events "
416  + " LEFT JOIN tsk_event_descriptions on ( tsk_events.event_description_id = tsk_event_descriptions.event_description_id ) "
417  + " WHERE artifact_id = " + artifact.getArtifactID();
419  try (CaseDbConnection con = caseDB.getConnection();
420  Statement stmt = con.createStatement();
421  ResultSet results = stmt.executeQuery(query);) {
422  while (results.next()) {
423  eventIDs.add(results.getLong("event_id"));//NON-NLS
424  }
425  } catch (SQLException ex) {
426  throw new TskCoreException("Error executing getEventIDsForArtifact query.", ex); // NON-NLS
427  } finally {
429  }
430  return eventIDs;
431  }
432 
446  public Set<Long> getEventIDsForContent(Content content, boolean includeDerivedArtifacts) throws TskCoreException {
448  try (CaseDbConnection conn = caseDB.getConnection()) {
449  return getEventAndDescriptionIDs(conn, content.getId(), includeDerivedArtifacts).keySet();
450  } finally {
452  }
453  }
454 
473  private Long addEventDescription(long dataSourceObjId, long fileObjId, Long artifactID,
474  String fullDescription, String medDescription, String shortDescription,
475  boolean hasHashHits, boolean tagged, CaseDbConnection connection) throws TskCoreException, DuplicateException {
476  String tableValuesClause
477  = "tsk_event_descriptions ( "
478  + "data_source_obj_id, content_obj_id, artifact_id, "
479  + " full_description, med_description, short_description, "
480  + " hash_hit, tagged "
481  + " ) VALUES "
482  + "(?, ?, ?, ?, ?, ?, ?, ?)";
483 
484  String insertDescriptionSql = getSqlIgnoreConflict(tableValuesClause);
485 
487  try {
488  PreparedStatement insertDescriptionStmt = connection.getPreparedStatement(insertDescriptionSql, PreparedStatement.RETURN_GENERATED_KEYS);
489  insertDescriptionStmt.clearParameters();
490  insertDescriptionStmt.setLong(1, dataSourceObjId);
491  insertDescriptionStmt.setLong(2, fileObjId);
492 
493  if (artifactID == null) {
494  insertDescriptionStmt.setNull(3, Types.INTEGER);
495  } else {
496  insertDescriptionStmt.setLong(3, artifactID);
497  }
498 
499  insertDescriptionStmt.setString(4, fullDescription);
500  insertDescriptionStmt.setString(5, medDescription);
501  insertDescriptionStmt.setString(6, shortDescription);
502  insertDescriptionStmt.setInt(7, booleanToInt(hasHashHits));
503  insertDescriptionStmt.setInt(8, booleanToInt(tagged));
504  int row = insertDescriptionStmt.executeUpdate();
505  // if no inserted rows, there is a conflict due to a duplicate event
506  // description. If that happens, return null as no id was inserted.
507  if (row < 1) {
508  return null;
509  }
510 
511  try (ResultSet generatedKeys = insertDescriptionStmt.getGeneratedKeys()) {
512  if (generatedKeys.next()) {
513  return generatedKeys.getLong(1);
514  } else {
515  return null;
516  }
517  }
518  } catch (SQLException ex) {
519  throw new TskCoreException("Failed to insert event description.", ex); // NON-NLS
520  } finally {
522  }
523  }
524 
538  private Long getEventDescription(long dataSourceObjId, long fileObjId, Long artifactID,
539  String fullDescription, CaseDbConnection connection) throws TskCoreException {
540 
541  String query = "SELECT event_description_id FROM tsk_event_descriptions "
542  + "WHERE data_source_obj_id = " + dataSourceObjId
543  + " AND content_obj_id = " + fileObjId
544  + " AND artifact_id " + (artifactID != null ? " = " + artifactID : "IS null")
545  + " AND full_description " + (fullDescription != null ? "= '"
546  + SleuthkitCase.escapeSingleQuotes(fullDescription) + "'" : "IS null");
547 
549  try (ResultSet resultSet = connection.createStatement().executeQuery(query)) {
550 
551  if (resultSet.next()) {
552  long id = resultSet.getLong(1);
553  return id;
554  }
555  } catch (SQLException ex) {
556  throw new TskCoreException(String.format("Failed to get description, dataSource=%d, fileObjId=%d, artifactId=%d", dataSourceObjId, fileObjId, artifactID), ex);
557  } finally {
559  }
560 
561  return null;
562  }
563 
564  Collection<TimelineEvent> addEventsForNewFile(AbstractFile file, CaseDbConnection connection) throws TskCoreException {
565  Set<TimelineEvent> events = addEventsForNewFileQuiet(file, connection);
566  events.stream()
567  .map(TimelineEventAddedEvent::new)
568  .forEach(caseDB::fireTSKEvent);
569 
570  return events;
571  }
572 
587  Set<TimelineEvent> addEventsForNewFileQuiet(AbstractFile file, CaseDbConnection connection) throws TskCoreException {
588  //gather time stamps into map
589  // if any of these events become deprecated in the future, filtering may need to occur.
590  Map<TimelineEventType, Long> timeMap = ImmutableMap.of(TimelineEventType.FILE_CREATED, file.getCrtime(),
591  TimelineEventType.FILE_ACCESSED, file.getAtime(),
592  TimelineEventType.FILE_CHANGED, file.getCtime(),
593  TimelineEventType.FILE_MODIFIED, file.getMtime());
594 
595  /*
596  * If there are no legitimate ( greater than zero ) time stamps skip the
597  * rest of the event generation.
598  */
599  if (Collections.max(timeMap.values()) <= 0) {
600  return Collections.emptySet();
601  }
602 
603  String description = file.getParentPath() + file.getName();
604  long fileObjId = file.getId();
605  Set<TimelineEvent> events = new HashSet<>();
607  try {
608  Long descriptionID = addEventDescription(file.getDataSourceObjectId(), fileObjId, null,
609  description, null, null, false, false, connection);
610 
611  if(descriptionID == null) {
612  descriptionID = getEventDescription(file.getDataSourceObjectId(), fileObjId, null, description, connection);
613  }
614  if(descriptionID != null) {
615  for (Map.Entry<TimelineEventType, Long> timeEntry : timeMap.entrySet()) {
616  Long time = timeEntry.getValue();
617  if (time > 0 && time < MAX_TIMESTAMP_TO_ADD) {// if the time is legitimate ( greater than zero and less then 12 years from current date) insert it
618  TimelineEventType type = timeEntry.getKey();
619  long eventID = addEventWithExistingDescription(time, type, descriptionID, connection);
620 
621  /*
622  * Last two flags indicating hasTags and hasHashHits are
623  * both set to false with the assumption that this is not
624  * possible for a new file. See JIRA-5407
625  */
626  events.add(new TimelineEvent(eventID, descriptionID, fileObjId, null, time, type,
627  description, null, null, false, false));
628  } else {
629  if (time >= MAX_TIMESTAMP_TO_ADD) {
630  logger.log(Level.WARNING, String.format("Date/Time discarded from Timeline for %s for file %s with Id %d", timeEntry.getKey().getDisplayName(), file.getParentPath() + file.getName(), file.getId()));
631  }
632  }
633  }
634  } else {
635  throw new TskCoreException(String.format("Failed to get event description for file id = %d", fileObjId));
636  }
637  } catch (DuplicateException dupEx) {
638  logger.log(Level.SEVERE, "Attempt to make file event duplicate.", dupEx);
639  } finally {
641  }
642 
643  return events;
644  }
645 
659  Set<TimelineEvent> addArtifactEvents(BlackboardArtifact artifact) throws TskCoreException {
660  Set<TimelineEvent> newEvents = new HashSet<>();
661 
662  /*
663  * If the artifact is a TSK_TL_EVENT, use the TSK_TL_EVENT_TYPE
664  * attribute to determine its event type, but give it a generic
665  * description.
666  */
667  if (artifact.getArtifactTypeID() == TSK_TL_EVENT.getTypeID()) {
668  TimelineEventType eventType;//the type of the event to add.
669  BlackboardAttribute attribute = artifact.getAttribute(new BlackboardAttribute.Type(TSK_TL_EVENT_TYPE));
670  if (attribute == null) {
671  eventType = TimelineEventType.STANDARD_ARTIFACT_CATCH_ALL;
672  } else {
673  long eventTypeID = attribute.getValueLong();
674  eventType = eventTypeIDMap.getOrDefault(eventTypeID, TimelineEventType.STANDARD_ARTIFACT_CATCH_ALL);
675  }
676 
677  try {
678  // @@@ This casting is risky if we change class hierarchy, but was expedient. Should move parsing to another class
679  addArtifactEvent(((TimelineEventArtifactTypeImpl) TimelineEventType.STANDARD_ARTIFACT_CATCH_ALL).makeEventDescription(artifact), eventType, artifact)
680  .ifPresent(newEvents::add);
681  } catch (DuplicateException ex) {
682  logger.log(Level.SEVERE, getDuplicateExceptionMessage(artifact, "Attempt to make a timeline event artifact duplicate"), ex);
683  }
684  } else {
685  /*
686  * If there are any event types configured to make descriptions
687  * automatically, use those.
688  */
689  Set<TimelineEventArtifactTypeImpl> eventTypesForArtifact = eventTypeIDMap.values().stream()
690  .filter(TimelineEventArtifactTypeImpl.class::isInstance)
691  .map(TimelineEventArtifactTypeImpl.class::cast)
692  .filter(eventType -> eventType.getArtifactTypeID() == artifact.getArtifactTypeID())
693  .collect(Collectors.toSet());
694 
695  boolean duplicateExists = false;
696  for (TimelineEventArtifactTypeImpl eventType : eventTypesForArtifact) {
697  try {
698  addArtifactEvent(eventType.makeEventDescription(artifact), eventType, artifact)
699  .ifPresent(newEvents::add);
700  } catch (DuplicateException ex) {
701  duplicateExists = true;
702  logger.log(Level.SEVERE, getDuplicateExceptionMessage(artifact, "Attempt to make artifact event duplicate"), ex);
703  }
704  }
705 
706  // if no other timeline events were created directly, then create new 'other' ones.
707  if (!duplicateExists && newEvents.isEmpty()) {
708  try {
709  addOtherEventDesc(artifact).ifPresent(newEvents::add);
710  } catch (DuplicateException ex) {
711  logger.log(Level.SEVERE, getDuplicateExceptionMessage(artifact, "Attempt to make 'other' artifact event duplicate"), ex);
712  }
713  }
714  }
715  newEvents.stream()
716  .map(TimelineEventAddedEvent::new)
717  .forEach(caseDB::fireTSKEvent);
718  return newEvents;
719  }
720 
733  private String getDuplicateExceptionMessage(BlackboardArtifact artifact, String error) {
734  String artifactIDStr = null;
735  String sourceStr = null;
736 
737  if (artifact != null) {
738  artifactIDStr = Long.toString(artifact.getId());
739 
740  try {
741  sourceStr = artifact.getAttributes().stream()
742  .filter(attr -> attr != null && attr.getSources() != null && !attr.getSources().isEmpty())
743  .map(attr -> String.join(",", attr.getSources()))
744  .findFirst()
745  .orElse(null);
746  } catch (TskCoreException ex) {
747  logger.log(Level.WARNING, String.format("Could not fetch artifacts for artifact id: %d.", artifact.getId()), ex);
748  }
749  }
750 
751  artifactIDStr = (artifactIDStr == null) ? "<null>" : artifactIDStr;
752  sourceStr = (sourceStr == null) ? "<null>" : sourceStr;
753 
754  return String.format("%s (artifactID=%s, Source=%s).", error, artifactIDStr, sourceStr);
755  }
756 
768  private Optional<TimelineEvent> addOtherEventDesc(BlackboardArtifact artifact) throws TskCoreException, DuplicateException {
769  if (artifact == null) {
770  return Optional.empty();
771  }
772 
773  Long timeVal = artifact.getAttributes().stream()
774  .filter((attr) -> attr.getAttributeType().getValueType() == BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME)
775  .map(attr -> attr.getValueLong())
776  .findFirst()
777  .orElse(null);
778 
779  if (timeVal == null) {
780  return Optional.empty();
781  }
782 
783  String description = String.format("%s: %d", artifact.getDisplayName(), artifact.getId());
784 
785  TimelineEventDescriptionWithTime evtWDesc = new TimelineEventDescriptionWithTime(timeVal, description, description, description);
786 
787  TimelineEventType evtType = (ARTIFACT_TYPE_IDS.contains(artifact.getArtifactTypeID()))
788  ? TimelineEventType.STANDARD_ARTIFACT_CATCH_ALL
789  : TimelineEventType.CUSTOM_ARTIFACT_CATCH_ALL;
790 
791  return addArtifactEvent(evtWDesc, evtType, artifact);
792  }
793 
807  private Optional<TimelineEvent> addArtifactEvent(TimelineEventDescriptionWithTime eventPayload,
808  TimelineEventType eventType, BlackboardArtifact artifact) throws TskCoreException, DuplicateException {
809 
810  // make sure event payload is present
811  // only create event for a timeline event type if not deprecated
812  if (eventPayload == null || eventType.isDeprecated()) {
813  return Optional.empty();
814  }
815  long time = eventPayload.getTime();
816  // if the time is legitimate ( greater than or equal to zero or less than or equal to 12 years from present time) insert it into the db
817  if (time <= 0 || time >= MAX_TIMESTAMP_TO_ADD) {
818  if (time >= MAX_TIMESTAMP_TO_ADD) {
819  logger.log(Level.WARNING, String.format("Date/Time discarded from Timeline for %s for artifact %s with id %d", artifact.getDisplayName(), eventPayload.getDescription(TimelineLevelOfDetail.HIGH), artifact.getId()));
820  }
821  return Optional.empty();
822  }
823  String fullDescription = eventPayload.getDescription(TimelineLevelOfDetail.HIGH);
824  String medDescription = eventPayload.getDescription(TimelineLevelOfDetail.MEDIUM);
825  String shortDescription = eventPayload.getDescription(TimelineLevelOfDetail.LOW);
826  long artifactID = artifact.getArtifactID();
827  long fileObjId = artifact.getObjectID();
828  Long dataSourceObjectID = artifact.getDataSourceObjectID();
829 
830  if(dataSourceObjectID == null) {
831  logger.log(Level.SEVERE, String.format("Failed to create timeline event for artifact (%d), artifact data source was null"), artifact.getId());
832  return Optional.empty();
833  }
834 
835  AbstractFile file = caseDB.getAbstractFileById(fileObjId);
836  boolean hasHashHits = false;
837  // file will be null if source was data source or some non-file
838  if (file != null) {
839  hasHashHits = isNotEmpty(file.getHashSetNames());
840  }
841  boolean tagged = isNotEmpty(caseDB.getBlackboardArtifactTagsByArtifact(artifact));
842 
843  TimelineEvent event;
845  try (CaseDbConnection connection = caseDB.getConnection();) {
846 
847  Long descriptionID = addEventDescription(dataSourceObjectID, fileObjId, artifactID,
848  fullDescription, medDescription, shortDescription,
849  hasHashHits, tagged, connection);
850 
851  if(descriptionID == null) {
852  descriptionID = getEventDescription(dataSourceObjectID, fileObjId, artifactID,
853  fullDescription, connection);
854  }
855 
856  if(descriptionID != null) {
857  long eventID = addEventWithExistingDescription(time, eventType, descriptionID, connection);
858 
859  event = new TimelineEvent(eventID, dataSourceObjectID, fileObjId, artifactID,
860  time, eventType, fullDescription, medDescription, shortDescription,
861  hasHashHits, tagged);
862  } else {
863  throw new TskCoreException(String.format("Failed to get event description for file id = %d, artifactId %d", fileObjId, artifactID));
864  }
865 
866  } finally {
868  }
869  return Optional.of(event);
870  }
871 
872  private long addEventWithExistingDescription(Long time, TimelineEventType type, long descriptionID, CaseDbConnection connection) throws TskCoreException, DuplicateException {
873  String tableValuesClause
874  = "tsk_events ( event_type_id, event_description_id , time) VALUES (?, ?, ?)";
875 
876  String insertEventSql = getSqlIgnoreConflict(tableValuesClause);
877 
879  try {
880  PreparedStatement insertRowStmt = connection.getPreparedStatement(insertEventSql, Statement.RETURN_GENERATED_KEYS);
881  insertRowStmt.clearParameters();
882  insertRowStmt.setLong(1, type.getTypeID());
883  insertRowStmt.setLong(2, descriptionID);
884  insertRowStmt.setLong(3, time);
885  int row = insertRowStmt.executeUpdate();
886  // if no inserted rows, return null.
887  if (row < 1) {
888  throw new DuplicateException(String.format("An event already exists in the event table for this item [time: %s, type: %s, description: %d].",
889  time == null ? "<null>" : Long.toString(time),
890  type == null ? "<null>" : type.toString(),
891  descriptionID));
892  }
893 
894  try (ResultSet generatedKeys = insertRowStmt.getGeneratedKeys();) {
895  if (generatedKeys.next()) {
896  return generatedKeys.getLong(1);
897  } else {
898  throw new DuplicateException(String.format("An event already exists in the event table for this item [time: %s, type: %s, description: %d].",
899  time == null ? "<null>" : Long.toString(time),
900  type == null ? "<null>" : type.toString(),
901  descriptionID));
902  }
903  }
904  } catch (SQLException ex) {
905  throw new TskCoreException("Failed to insert event for existing description.", ex); // NON-NLS
906  } finally {
908  }
909  }
910 
911  private Map<Long, Long> getEventAndDescriptionIDs(CaseDbConnection conn, long contentObjID, boolean includeArtifacts) throws TskCoreException {
912  return getEventAndDescriptionIDsHelper(conn, contentObjID, (includeArtifacts ? "" : " AND artifact_id IS NULL"));
913  }
914 
915  private Map<Long, Long> getEventAndDescriptionIDs(CaseDbConnection conn, long contentObjID, Long artifactID) throws TskCoreException {
916  return getEventAndDescriptionIDsHelper(conn, contentObjID, " AND artifact_id = " + artifactID);
917  }
918 
919  private Map<Long, Long> getEventAndDescriptionIDsHelper(CaseDbConnection con, long fileObjID, String artifactClause) throws TskCoreException {
920  //map from event_id to the event_description_id for that event.
921  Map<Long, Long> eventIDToDescriptionIDs = new HashMap<>();
922  String sql = "SELECT event_id, tsk_events.event_description_id"
923  + " FROM tsk_events "
924  + " LEFT JOIN tsk_event_descriptions ON ( tsk_events.event_description_id = tsk_event_descriptions.event_description_id )"
925  + " WHERE content_obj_id = " + fileObjID
926  + artifactClause;
927  try (Statement selectStmt = con.createStatement(); ResultSet executeQuery = selectStmt.executeQuery(sql);) {
928  while (executeQuery.next()) {
929  eventIDToDescriptionIDs.put(executeQuery.getLong("event_id"), executeQuery.getLong("event_description_id")); //NON-NLS
930  }
931  } catch (SQLException ex) {
932  throw new TskCoreException("Error getting event description ids for object id = " + fileObjID, ex);
933  }
934  return eventIDToDescriptionIDs;
935  }
936 
953  @Beta
954  public Set<Long> updateEventsForContentTagAdded(Content content) throws TskCoreException {
956  try (CaseDbConnection conn = caseDB.getConnection()) {
957  Map<Long, Long> eventIDs = getEventAndDescriptionIDs(conn, content.getId(), false);
958  updateEventSourceTaggedFlag(conn, eventIDs.values(), 1);
959  return eventIDs.keySet();
960  } finally {
962  }
963  }
964 
982  @Beta
983  public Set<Long> updateEventsForContentTagDeleted(Content content) throws TskCoreException {
985  try (CaseDbConnection conn = caseDB.getConnection()) {
986  if (caseDB.getContentTagsByContent(content).isEmpty()) {
987  Map<Long, Long> eventIDs = getEventAndDescriptionIDs(conn, content.getId(), false);
988  updateEventSourceTaggedFlag(conn, eventIDs.values(), 0);
989  return eventIDs.keySet();
990  } else {
991  return Collections.emptySet();
992  }
993  } finally {
995  }
996  }
997 
1009  public Set<Long> updateEventsForArtifactTagAdded(BlackboardArtifact artifact) throws TskCoreException {
1011  try (CaseDbConnection conn = caseDB.getConnection()) {
1012  Map<Long, Long> eventIDs = getEventAndDescriptionIDs(conn, artifact.getObjectID(), artifact.getArtifactID());
1013  updateEventSourceTaggedFlag(conn, eventIDs.values(), 1);
1014  return eventIDs.keySet();
1015  } finally {
1017  }
1018  }
1019 
1032  public Set<Long> updateEventsForArtifactTagDeleted(BlackboardArtifact artifact) throws TskCoreException {
1034  try (CaseDbConnection conn = caseDB.getConnection()) {
1035  if (caseDB.getBlackboardArtifactTagsByArtifact(artifact).isEmpty()) {
1036  Map<Long, Long> eventIDs = getEventAndDescriptionIDs(conn, artifact.getObjectID(), artifact.getArtifactID());
1037  updateEventSourceTaggedFlag(conn, eventIDs.values(), 0);
1038  return eventIDs.keySet();
1039  } else {
1040  return Collections.emptySet();
1041  }
1042  } finally {
1044  }
1045  }
1046 
1047  private void updateEventSourceTaggedFlag(CaseDbConnection conn, Collection<Long> eventDescriptionIDs, int flagValue) throws TskCoreException {
1048  if (eventDescriptionIDs.isEmpty()) {
1049  return;
1050  }
1051 
1052  String sql = "UPDATE tsk_event_descriptions SET tagged = " + flagValue + " WHERE event_description_id IN (" + buildCSVString(eventDescriptionIDs) + ")"; //NON-NLS
1053  try (Statement updateStatement = conn.createStatement()) {
1054  updateStatement.executeUpdate(sql);
1055  } catch (SQLException ex) {
1056  throw new TskCoreException("Error marking content events tagged: " + sql, ex);//NON-NLS
1057  }
1058  }
1059 
1074  public Set<Long> updateEventsForHashSetHit(Content content) throws TskCoreException {
1076  try (CaseDbConnection con = caseDB.getConnection(); Statement updateStatement = con.createStatement();) {
1077  Map<Long, Long> eventIDs = getEventAndDescriptionIDs(con, content.getId(), true);
1078  if (!eventIDs.isEmpty()) {
1079  String sql = "UPDATE tsk_event_descriptions SET hash_hit = 1" + " WHERE event_description_id IN (" + buildCSVString(eventIDs.values()) + ")"; //NON-NLS
1080  try {
1081  updateStatement.executeUpdate(sql); //NON-NLS
1082  return eventIDs.keySet();
1083  } catch (SQLException ex) {
1084  throw new TskCoreException("Error setting hash_hit of events.", ex);//NON-NLS
1085  }
1086  } else {
1087  return eventIDs.keySet();
1088  }
1089  } catch (SQLException ex) {
1090  throw new TskCoreException("Error setting hash_hit of events.", ex);//NON-NLS
1091  } finally {
1093  }
1094  }
1095 
1096  void rollBackTransaction(SleuthkitCase.CaseDbTransaction trans) throws TskCoreException {
1097  trans.rollback();
1098  }
1099 
1119  public Map<TimelineEventType, Long> countEventsByType(Long startTime, Long endTime, TimelineFilter.RootFilter filter, TimelineEventType.HierarchyLevel typeHierachyLevel) throws TskCoreException {
1120  long adjustedEndTime = Objects.equals(startTime, endTime) ? endTime + 1 : endTime;
1121  //do we want the base or subtype column of the databse
1122  String typeColumn = typeColumnHelper(TimelineEventType.HierarchyLevel.EVENT.equals(typeHierachyLevel));
1123 
1124  String queryString = "SELECT count(DISTINCT tsk_events.event_id) AS count, " + typeColumn//NON-NLS
1125  + " FROM " + getAugmentedEventsTablesSQL(filter)//NON-NLS
1126  + " WHERE time >= " + startTime + " AND time < " + adjustedEndTime + " AND " + getSQLWhere(filter) // NON-NLS
1127  + " GROUP BY " + typeColumn; // NON-NLS
1128 
1130  try (CaseDbConnection con = caseDB.getConnection();
1131  Statement stmt = con.createStatement();
1132  ResultSet results = stmt.executeQuery(queryString);) {
1133  Map<TimelineEventType, Long> typeMap = new HashMap<>();
1134  while (results.next()) {
1135  int eventTypeID = results.getInt(typeColumn);
1136  TimelineEventType eventType = getEventType(eventTypeID)
1137  .orElseThrow(() -> newEventTypeMappingException(eventTypeID));//NON-NLS
1138 
1139  typeMap.put(eventType, results.getLong("count")); // NON-NLS
1140  }
1141  return typeMap;
1142  } catch (SQLException ex) {
1143  throw new TskCoreException("Error getting count of events from db: " + queryString, ex); // NON-NLS
1144  } finally {
1146  }
1147  }
1148 
1149  private static TskCoreException newEventTypeMappingException(int eventTypeID) {
1150  return new TskCoreException("Error mapping event type id " + eventTypeID + " to EventType.");//NON-NLS
1151  }
1152 
1166  static private String getAugmentedEventsTablesSQL(TimelineFilter.RootFilter filter) {
1167  TimelineFilter.FileTypesFilter fileTypesFitler = filter.getFileTypesFilter();
1168  boolean needsMimeTypes = fileTypesFitler != null && fileTypesFitler.hasSubFilters();
1169 
1170  return getAugmentedEventsTablesSQL(needsMimeTypes);
1171  }
1172 
1187  static private String getAugmentedEventsTablesSQL(boolean needMimeTypes) {
1188  /*
1189  * Regarding the timeline event tables schema, note that several columns
1190  * in the tsk_event_descriptions table seem, at first glance, to be
1191  * attributes of events rather than their descriptions and would appear
1192  * to belong in tsk_events table instead. The rationale for putting the
1193  * data source object ID, content object ID, artifact ID and the flags
1194  * indicating whether or not the event source has a hash set hit or is
1195  * tagged were motivated by the fact that these attributes are identical
1196  * for each event in a set of file system file MAC time events. The
1197  * decision was made to avoid duplication and save space by placing this
1198  * data in the tsk_event-descriptions table.
1199  */
1200  return "( SELECT event_id, time, tsk_event_descriptions.data_source_obj_id, content_obj_id, artifact_id, "
1201  + " full_description, med_description, short_description, tsk_events.event_type_id, super_type_id,"
1202  + " hash_hit, tagged "
1203  + (needMimeTypes ? ", mime_type" : "")
1204  + " FROM tsk_events "
1205  + " JOIN tsk_event_descriptions ON ( tsk_event_descriptions.event_description_id = tsk_events.event_description_id)"
1206  + " JOIN tsk_event_types ON (tsk_events.event_type_id = tsk_event_types.event_type_id ) "
1207  + (needMimeTypes ? " LEFT OUTER JOIN tsk_files "
1208  + " ON (tsk_event_descriptions.content_obj_id = tsk_files.obj_id)"
1209  : "")
1210  + ") AS tsk_events";
1211  }
1212 
1220  private static int booleanToInt(boolean value) {
1221  return value ? 1 : 0;
1222  }
1223 
1224  private static boolean intToBoolean(int value) {
1225  return value != 0;
1226  }
1227 
1240  public List<TimelineEvent> getEvents(Interval timeRange, TimelineFilter.RootFilter filter) throws TskCoreException {
1241  List<TimelineEvent> events = new ArrayList<>();
1242 
1243  Long startTime = timeRange.getStartMillis() / 1000;
1244  Long endTime = timeRange.getEndMillis() / 1000;
1245 
1246  if (Objects.equals(startTime, endTime)) {
1247  endTime++; //make sure end is at least 1 millisecond after start
1248  }
1249 
1250  if (filter == null) {
1251  return events;
1252  }
1253 
1254  if (endTime < startTime) {
1255  return events;
1256  }
1257 
1258  //build dynamic parts of query
1259  String querySql = "SELECT time, content_obj_id, data_source_obj_id, artifact_id, " // NON-NLS
1260  + " event_id, " //NON-NLS
1261  + " hash_hit, " //NON-NLS
1262  + " tagged, " //NON-NLS
1263  + " event_type_id, super_type_id, "
1264  + " full_description, med_description, short_description " // NON-NLS
1265  + " FROM " + getAugmentedEventsTablesSQL(filter) // NON-NLS
1266  + " WHERE time >= " + startTime + " AND time < " + endTime + " AND " + getSQLWhere(filter) // NON-NLS
1267  + " ORDER BY time"; // NON-NLS
1268 
1270  try (CaseDbConnection con = caseDB.getConnection();
1271  Statement stmt = con.createStatement();
1272  ResultSet resultSet = stmt.executeQuery(querySql);) {
1273 
1274  while (resultSet.next()) {
1275  int eventTypeID = resultSet.getInt("event_type_id");
1276  TimelineEventType eventType = getEventType(eventTypeID).orElseThrow(()
1277  -> new TskCoreException("Error mapping event type id " + eventTypeID + "to EventType."));//NON-NLS
1278 
1279  TimelineEvent event = new TimelineEvent(
1280  resultSet.getLong("event_id"), // NON-NLS
1281  resultSet.getLong("data_source_obj_id"), // NON-NLS
1282  resultSet.getLong("content_obj_id"), // NON-NLS
1283  resultSet.getLong("artifact_id"), // NON-NLS
1284  resultSet.getLong("time"), // NON-NLS
1285  eventType,
1286  resultSet.getString("full_description"), // NON-NLS
1287  resultSet.getString("med_description"), // NON-NLS
1288  resultSet.getString("short_description"), // NON-NLS
1289  resultSet.getInt("hash_hit") != 0, //NON-NLS
1290  resultSet.getInt("tagged") != 0);
1291 
1292  events.add(event);
1293  }
1294 
1295  } catch (SQLException ex) {
1296  throw new TskCoreException("Error getting events from db: " + querySql, ex); // NON-NLS
1297  } finally {
1299  }
1300 
1301  return events;
1302  }
1303 
1311  private static String typeColumnHelper(final boolean useSubTypes) {
1312  return useSubTypes ? "event_type_id" : "super_type_id"; //NON-NLS
1313  }
1314 
1323  String getSQLWhere(TimelineFilter.RootFilter filter) {
1324 
1325  String result;
1326  if (filter == null) {
1327  return getTrueLiteral();
1328  } else {
1329  result = filter.getSQLWhere(this);
1330  }
1331 
1332  return result;
1333  }
1334 
1346  private String getSqlIgnoreConflict(String insertTableValues) throws TskCoreException {
1347  switch (caseDB.getDatabaseType()) {
1348  case POSTGRESQL:
1349  return "INSERT INTO " + insertTableValues + " ON CONFLICT DO NOTHING";
1350  case SQLITE:
1351  return "INSERT OR IGNORE INTO " + insertTableValues;
1352  default:
1353  throw new TskCoreException("Unknown DB Type: " + caseDB.getDatabaseType().name());
1354  }
1355  }
1356 
1357  private String getTrueLiteral() {
1358  switch (caseDB.getDatabaseType()) {
1359  case POSTGRESQL:
1360  return "TRUE";//NON-NLS
1361  case SQLITE:
1362  return "1";//NON-NLS
1363  default:
1364  throw new UnsupportedOperationException("Unsupported DB type: " + caseDB.getDatabaseType().name());//NON-NLS
1365 
1366  }
1367  }
1368 
1373  final static public class TimelineEventAddedEvent {
1374 
1375  private final TimelineEvent addedEvent;
1376 
1378  return addedEvent;
1379  }
1380 
1382  this.addedEvent = event;
1383  }
1384  }
1385 
1389  private static class DuplicateException extends Exception {
1390 
1391  private static final long serialVersionUID = 1L;
1392 
1398  DuplicateException(String message) {
1399  super(message);
1400  }
1401  }
1402 }
List< Long > getEventIDs(Interval timeRange, TimelineFilter.RootFilter filter)
TimelineEvent getEventById(long eventID)
ImmutableList< TimelineEventType > getEventTypes()
Interval getSpanningInterval(Interval timeRange, TimelineFilter.RootFilter filter, DateTimeZone timeZone)
Set< Long > getEventIDsForContent(Content content, boolean includeDerivedArtifacts)
Interval getSpanningInterval(Collection< Long > eventIDs)
Set< Long > updateEventsForContentTagAdded(Content content)
List< BlackboardArtifactTag > getBlackboardArtifactTagsByArtifact(BlackboardArtifact artifact)
SortedSet<?extends TimelineEventType > getChildren()
Set< Long > updateEventsForContentTagDeleted(Content content)
Set< Long > updateEventsForHashSetHit(Content content)
static String escapeSingleQuotes(String text)
Set< Long > updateEventsForArtifactTagDeleted(BlackboardArtifact artifact)
Map< TimelineEventType, Long > countEventsByType(Long startTime, Long endTime, TimelineFilter.RootFilter filter, TimelineEventType.HierarchyLevel typeHierachyLevel)
List< Long > getEventIDsForArtifact(BlackboardArtifact artifact)
List< TimelineEvent > getEvents(Interval timeRange, TimelineFilter.RootFilter filter)
List< ContentTag > getContentTagsByContent(Content content)
Optional< TimelineEventType > getEventType(long eventTypeID)
Set< Long > updateEventsForArtifactTagAdded(BlackboardArtifact artifact)

Copyright © 2011-2021 Brian Carrier. (carrier -at- sleuthkit -dot- org)
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.