Sleuth Kit Java Bindings (JNI)  4.10.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;
51 import static org.sleuthkit.datamodel.StringUtils.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>()
86  .build();
87 
88  // all known artifact type ids (used for determining if an artifact is standard or custom event)
89  private static final Set<Integer> ARTIFACT_TYPE_IDS = Stream.of(BlackboardArtifact.ARTIFACT_TYPE.values())
90  .map(artType -> artType.getTypeID())
91  .collect(Collectors.toSet());
92 
93  private final SleuthkitCase caseDB;
94 
99  private static final Long MAX_TIMESTAMP_TO_ADD = Instant.now().getEpochSecond() + 394200000;
100 
104  private final Map<Long, TimelineEventType> eventTypeIDMap = new HashMap<>();
105 
116  this.caseDB = caseDB;
117 
118  //initialize root and base event types, these are added to the DB in c++ land
119  ROOT_CATEGORY_AND_FILESYSTEM_TYPES.forEach(eventType -> eventTypeIDMap.put(eventType.getTypeID(), eventType));
120 
121  //initialize the other event types that aren't added in c++
123  try (final CaseDbConnection con = caseDB.getConnection();
124  final Statement statement = con.createStatement()) {
125  for (TimelineEventType type : PREDEFINED_EVENT_TYPES) {
126  con.executeUpdate(statement,
127  insertOrIgnore(" INTO tsk_event_types(event_type_id, display_name, super_type_id) "
128  + "VALUES( " + type.getTypeID() + ", '"
129  + escapeSingleQuotes(type.getDisplayName()) + "',"
130  + type.getParent().getTypeID()
131  + ")")); //NON-NLS
132  eventTypeIDMap.put(type.getTypeID(), type);
133  }
134  } catch (SQLException ex) {
135  throw new TskCoreException("Failed to initialize timeline event types", ex); // NON-NLS
136  } finally {
138  }
139  }
140 
152  public Interval getSpanningInterval(Collection<Long> eventIDs) throws TskCoreException {
153  if (eventIDs.isEmpty()) {
154  return null;
155  }
156  final String query = "SELECT Min(time) as minTime, Max(time) as maxTime FROM tsk_events WHERE event_id IN (" + buildCSVString(eventIDs) + ")"; //NON-NLS
158  try (CaseDbConnection con = caseDB.getConnection();
159  Statement stmt = con.createStatement();
160  ResultSet results = stmt.executeQuery(query);) {
161  if (results.next()) {
162  return new Interval(results.getLong("minTime") * 1000, (results.getLong("maxTime") + 1) * 1000, DateTimeZone.UTC); // NON-NLS
163  }
164  } catch (SQLException ex) {
165  throw new TskCoreException("Error executing get spanning interval query: " + query, ex); // NON-NLS
166  } finally {
168  }
169  return null;
170  }
171 
184  public Interval getSpanningInterval(Interval timeRange, TimelineFilter.RootFilter filter, DateTimeZone timeZone) throws TskCoreException {
185  long start = timeRange.getStartMillis() / 1000;
186  long end = timeRange.getEndMillis() / 1000;
187  String sqlWhere = getSQLWhere(filter);
188  String augmentedEventsTablesSQL = getAugmentedEventsTablesSQL(filter);
189  String queryString = " SELECT (SELECT Max(time) FROM " + augmentedEventsTablesSQL
190  + " WHERE time <=" + start + " AND " + sqlWhere + ") AS start,"
191  + " (SELECT Min(time) FROM " + augmentedEventsTablesSQL
192  + " WHERE time >= " + end + " AND " + sqlWhere + ") AS end";//NON-NLS
194  try (CaseDbConnection con = caseDB.getConnection();
195  Statement stmt = con.createStatement(); //can't use prepared statement because of complex where clause
196  ResultSet results = stmt.executeQuery(queryString);) {
197 
198  if (results.next()) {
199  long start2 = results.getLong("start"); // NON-NLS
200  long end2 = results.getLong("end"); // NON-NLS
201 
202  if (end2 == 0) {
203  end2 = getMaxEventTime();
204  }
205  return new Interval(start2 * 1000, (end2 + 1) * 1000, timeZone);
206  }
207  } catch (SQLException ex) {
208  throw new TskCoreException("Failed to get MIN time.", ex); // NON-NLS
209  } finally {
211  }
212  return null;
213  }
214 
224  public TimelineEvent getEventById(long eventID) throws TskCoreException {
225  String sql = "SELECT * FROM " + getAugmentedEventsTablesSQL(false) + " WHERE event_id = " + eventID;
227  try (CaseDbConnection con = caseDB.getConnection();
228  Statement stmt = con.createStatement();) {
229  try (ResultSet results = stmt.executeQuery(sql);) {
230  if (results.next()) {
231  int typeID = results.getInt("event_type_id");
232  TimelineEventType type = getEventType(typeID).orElseThrow(() -> newEventTypeMappingException(typeID)); //NON-NLS
233  return new TimelineEvent(eventID,
234  results.getLong("data_source_obj_id"),
235  results.getLong("content_obj_id"),
236  results.getLong("artifact_id"),
237  results.getLong("time"),
238  type, results.getString("full_description"),
239  results.getString("med_description"),
240  results.getString("short_description"),
241  intToBoolean(results.getInt("hash_hit")),
242  intToBoolean(results.getInt("tagged")));
243  }
244  }
245  } catch (SQLException sqlEx) {
246  throw new TskCoreException("Error while executing query " + sql, sqlEx); // NON-NLS
247  } finally {
249  }
250  return null;
251  }
252 
264  public List<Long> getEventIDs(Interval timeRange, TimelineFilter.RootFilter filter) throws TskCoreException {
265  Long startTime = timeRange.getStartMillis() / 1000;
266  Long endTime = timeRange.getEndMillis() / 1000;
267 
268  if (Objects.equals(startTime, endTime)) {
269  endTime++; //make sure end is at least 1 millisecond after start
270  }
271 
272  ArrayList<Long> resultIDs = new ArrayList<>();
273 
274  String query = "SELECT tsk_events.event_id AS event_id FROM " + getAugmentedEventsTablesSQL(filter)
275  + " WHERE time >= " + startTime + " AND time <" + endTime + " AND " + getSQLWhere(filter) + " ORDER BY time ASC"; // NON-NLS
277  try (CaseDbConnection con = caseDB.getConnection();
278  Statement stmt = con.createStatement();
279  ResultSet results = stmt.executeQuery(query);) {
280  while (results.next()) {
281  resultIDs.add(results.getLong("event_id")); //NON-NLS
282  }
283 
284  } catch (SQLException sqlEx) {
285  throw new TskCoreException("Error while executing query " + query, sqlEx); // NON-NLS
286  } finally {
288  }
289 
290  return resultIDs;
291  }
292 
301  public Long getMaxEventTime() throws TskCoreException {
303  try (CaseDbConnection con = caseDB.getConnection();
304  Statement stms = con.createStatement();
305  ResultSet results = stms.executeQuery(STATEMENTS.GET_MAX_TIME.getSQL());) {
306  if (results.next()) {
307  return results.getLong("max"); // NON-NLS
308  }
309  } catch (SQLException ex) {
310  throw new TskCoreException("Error while executing query " + STATEMENTS.GET_MAX_TIME.getSQL(), ex); // NON-NLS
311  } finally {
313  }
314  return -1l;
315  }
316 
325  public Long getMinEventTime() throws TskCoreException {
327  try (CaseDbConnection con = caseDB.getConnection();
328  Statement stms = con.createStatement();
329  ResultSet results = stms.executeQuery(STATEMENTS.GET_MIN_TIME.getSQL());) {
330  if (results.next()) {
331  return results.getLong("min"); // NON-NLS
332  }
333  } catch (SQLException ex) {
334  throw new TskCoreException("Error while executing query " + STATEMENTS.GET_MAX_TIME.getSQL(), ex); // NON-NLS
335  } finally {
337  }
338  return -1l;
339  }
340 
349  public Optional<TimelineEventType> getEventType(long eventTypeID) {
350  return Optional.ofNullable(eventTypeIDMap.get(eventTypeID));
351  }
352 
358  public ImmutableList<TimelineEventType> getEventTypes() {
359  return ImmutableList.copyOf(eventTypeIDMap.values());
360  }
361 
362  private String insertOrIgnore(String query) {
363  switch (caseDB.getDatabaseType()) {
364  case POSTGRESQL:
365  return " INSERT " + query + " ON CONFLICT DO NOTHING "; //NON-NLS
366  case SQLITE:
367  return " INSERT OR IGNORE " + query; //NON-NLS
368  default:
369  throw new UnsupportedOperationException("Unsupported DB type: " + caseDB.getDatabaseType().name());
370  }
371  }
372 
376  private enum STATEMENTS {
377 
378  GET_MAX_TIME("SELECT Max(time) AS max FROM tsk_events"), // NON-NLS
379  GET_MIN_TIME("SELECT Min(time) AS min FROM tsk_events"); // NON-NLS
380 
381  private final String sql;
382 
383  private STATEMENTS(String sql) {
384  this.sql = sql;
385  }
386 
387  String getSQL() {
388  return sql;
389  }
390  }
391 
402  public List<Long> getEventIDsForArtifact(BlackboardArtifact artifact) throws TskCoreException {
403  ArrayList<Long> eventIDs = new ArrayList<>();
404 
405  String query
406  = "SELECT event_id FROM tsk_events "
407  + " LEFT JOIN tsk_event_descriptions on ( tsk_events.event_description_id = tsk_event_descriptions.event_description_id ) "
408  + " WHERE artifact_id = " + artifact.getArtifactID();
410  try (CaseDbConnection con = caseDB.getConnection();
411  Statement stmt = con.createStatement();
412  ResultSet results = stmt.executeQuery(query);) {
413  while (results.next()) {
414  eventIDs.add(results.getLong("event_id"));//NON-NLS
415  }
416  } catch (SQLException ex) {
417  throw new TskCoreException("Error executing getEventIDsForArtifact query.", ex); // NON-NLS
418  } finally {
420  }
421  return eventIDs;
422  }
423 
437  public Set<Long> getEventIDsForContent(Content content, boolean includeDerivedArtifacts) throws TskCoreException {
439  try (CaseDbConnection conn = caseDB.getConnection()) {
440  return getEventAndDescriptionIDs(conn, content.getId(), includeDerivedArtifacts).keySet();
441  } finally {
443  }
444  }
445 
464  private long addEventDescription(long dataSourceObjId, long fileObjId, Long artifactID,
465  String fullDescription, String medDescription, String shortDescription,
466  boolean hasHashHits, boolean tagged, CaseDbConnection connection) throws TskCoreException, DuplicateException {
467  String tableValuesClause
468  = "tsk_event_descriptions ( "
469  + "data_source_obj_id, content_obj_id, artifact_id, "
470  + " full_description, med_description, short_description, "
471  + " hash_hit, tagged "
472  + " ) VALUES "
473  + "(?, ?, ?, ?, ?, ?, ?, ?)";
474 
475  String insertDescriptionSql = getSqlIgnoreConflict(tableValuesClause);
476 
478  try (PreparedStatement insertDescriptionStmt = connection.prepareStatement(insertDescriptionSql, PreparedStatement.RETURN_GENERATED_KEYS)) {
479  insertDescriptionStmt.clearParameters();
480  insertDescriptionStmt.setLong(1, dataSourceObjId);
481  insertDescriptionStmt.setLong(2, fileObjId);
482 
483  if (artifactID == null) {
484  insertDescriptionStmt.setNull(3, Types.INTEGER);
485  } else {
486  insertDescriptionStmt.setLong(3, artifactID);
487  }
488 
489  insertDescriptionStmt.setString(4, fullDescription);
490  insertDescriptionStmt.setString(5, medDescription);
491  insertDescriptionStmt.setString(6, shortDescription);
492  insertDescriptionStmt.setInt(7, booleanToInt(hasHashHits));
493  insertDescriptionStmt.setInt(8, booleanToInt(tagged));
494  int row = insertDescriptionStmt.executeUpdate();
495  // if no inserted rows, there is a conflict due to a duplicate event
496  // description. If that happens, return null as no id was inserted.
497  if (row < 1) {
498  throw new DuplicateException(String.format(
499  "An event description already exists for [fullDescription: %s, contentId: %d, artifactId: %s]",
500  fullDescription == null ? "<null>" : fullDescription,
501  fileObjId,
502  artifactID == null ? "<null>" : Long.toString(artifactID)));
503  }
504 
505  try (ResultSet generatedKeys = insertDescriptionStmt.getGeneratedKeys()) {
506  if (generatedKeys.next()) {
507  return generatedKeys.getLong(1);
508  } else {
509  throw new DuplicateException(String.format(
510  "An event description already exists for [fullDescription: %s, contentId: %d, artifactId: %s]",
511  fullDescription == null ? "<null>" : fullDescription,
512  fileObjId,
513  artifactID == null ? "<null>" : Long.toString(artifactID)));
514  }
515  }
516  } catch (SQLException ex) {
517  throw new TskCoreException("Failed to insert event description.", ex); // NON-NLS
518  } finally {
520  }
521  }
522 
523  Collection<TimelineEvent> addEventsForNewFile(AbstractFile file, CaseDbConnection connection) throws TskCoreException {
524  Set<TimelineEvent> events = addEventsForNewFileQuiet(file, connection);
525  events.stream()
526  .map(TimelineEventAddedEvent::new)
527  .forEach(caseDB::fireTSKEvent);
528 
529  return events;
530  }
531 
546  Set<TimelineEvent> addEventsForNewFileQuiet(AbstractFile file, CaseDbConnection connection) throws TskCoreException {
547  //gather time stamps into map
548  Map<TimelineEventType, Long> timeMap = ImmutableMap.of(TimelineEventType.FILE_CREATED, file.getCrtime(),
549  TimelineEventType.FILE_ACCESSED, file.getAtime(),
550  TimelineEventType.FILE_CHANGED, file.getCtime(),
551  TimelineEventType.FILE_MODIFIED, file.getMtime());
552 
553  /*
554  * If there are no legitimate ( greater than zero ) time stamps skip the
555  * rest of the event generation.
556  */
557  if (Collections.max(timeMap.values()) <= 0) {
558  return Collections.emptySet();
559  }
560 
561  String description = file.getParentPath() + file.getName();
562  long fileObjId = file.getId();
563  Set<TimelineEvent> events = new HashSet<>();
565  try {
566  long descriptionID = addEventDescription(file.getDataSourceObjectId(), fileObjId, null,
567  description, null, null, false, false, connection);
568 
569  for (Map.Entry<TimelineEventType, Long> timeEntry : timeMap.entrySet()) {
570  Long time = timeEntry.getValue();
571  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
572  TimelineEventType type = timeEntry.getKey();
573  long eventID = addEventWithExistingDescription(time, type, descriptionID, connection);
574 
575  /*
576  * Last two flags indicating hasTags and hasHashHits are
577  * both set to false with the assumption that this is not
578  * possible for a new file. See JIRA-5407
579  */
580  events.add(new TimelineEvent(eventID, descriptionID, fileObjId, null, time, type,
581  description, null, null, false, false));
582  } else {
583  if (time >= MAX_TIMESTAMP_TO_ADD) {
584  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()));
585  }
586  }
587  }
588  } catch (DuplicateException dupEx) {
589  logger.log(Level.SEVERE, "Attempt to make file event duplicate.", dupEx);
590  } finally {
592  }
593 
594  return events;
595  }
596 
610  Set<TimelineEvent> addArtifactEvents(BlackboardArtifact artifact) throws TskCoreException {
611  Set<TimelineEvent> newEvents = new HashSet<>();
612 
613  /*
614  * If the artifact is a TSK_TL_EVENT, use the TSK_TL_EVENT_TYPE
615  * attribute to determine its event type, but give it a generic
616  * description.
617  */
618  if (artifact.getArtifactTypeID() == TSK_TL_EVENT.getTypeID()) {
619  TimelineEventType eventType;//the type of the event to add.
620  BlackboardAttribute attribute = artifact.getAttribute(new BlackboardAttribute.Type(TSK_TL_EVENT_TYPE));
621  if (attribute == null) {
622  eventType = TimelineEventType.OTHER;
623  } else {
624  long eventTypeID = attribute.getValueLong();
625  eventType = eventTypeIDMap.getOrDefault(eventTypeID, TimelineEventType.OTHER);
626  }
627 
628  try {
629  // @@@ This casting is risky if we change class hierarchy, but was expedient. Should move parsing to another class
630  addArtifactEvent(((TimelineEventArtifactTypeImpl) TimelineEventType.OTHER).makeEventDescription(artifact), eventType, artifact)
631  .ifPresent(newEvents::add);
632  } catch (DuplicateException ex) {
633  logger.log(Level.SEVERE, getDuplicateExceptionMessage(artifact, "Attempt to make a timeline event artifact duplicate"), ex);
634  }
635  } else {
636  /*
637  * If there are any event types configured to make descriptions
638  * automatically, use those.
639  */
640  Set<TimelineEventArtifactTypeImpl> eventTypesForArtifact = eventTypeIDMap.values().stream()
641  .filter(TimelineEventArtifactTypeImpl.class::isInstance)
642  .map(TimelineEventArtifactTypeImpl.class::cast)
643  .filter(eventType -> eventType.getArtifactTypeID() == artifact.getArtifactTypeID())
644  .collect(Collectors.toSet());
645 
646  boolean duplicateExists = false;
647  for (TimelineEventArtifactTypeImpl eventType : eventTypesForArtifact) {
648  try {
649  addArtifactEvent(eventType.makeEventDescription(artifact), eventType, artifact)
650  .ifPresent(newEvents::add);
651  } catch (DuplicateException ex) {
652  duplicateExists = true;
653  logger.log(Level.SEVERE, getDuplicateExceptionMessage(artifact, "Attempt to make artifact event duplicate"), ex);
654  }
655  }
656 
657  // if no other timeline events were created directly, then create new 'other' ones.
658  if (!duplicateExists && newEvents.isEmpty()) {
659  try {
660  addOtherEventDesc(artifact).ifPresent(newEvents::add);
661  } catch (DuplicateException ex) {
662  logger.log(Level.SEVERE, getDuplicateExceptionMessage(artifact, "Attempt to make 'other' artifact event duplicate"), ex);
663  }
664  }
665  }
666  newEvents.stream()
667  .map(TimelineEventAddedEvent::new)
668  .forEach(caseDB::fireTSKEvent);
669  return newEvents;
670  }
671 
684  private String getDuplicateExceptionMessage(BlackboardArtifact artifact, String error) {
685  String artifactIDStr = null;
686  String sourceStr = null;
687 
688  if (artifact != null) {
689  artifactIDStr = Long.toString(artifact.getId());
690 
691  try {
692  sourceStr = artifact.getAttributes().stream()
693  .filter(attr -> attr != null && attr.getSources() != null && !attr.getSources().isEmpty())
694  .map(attr -> String.join(",", attr.getSources()))
695  .findFirst()
696  .orElse(null);
697  } catch (TskCoreException ex) {
698  logger.log(Level.WARNING, String.format("Could not fetch artifacts for artifact id: %d.", artifact.getId()), ex);
699  }
700  }
701 
702  artifactIDStr = (artifactIDStr == null) ? "<null>" : artifactIDStr;
703  sourceStr = (sourceStr == null) ? "<null>" : sourceStr;
704 
705  return String.format("%s (artifactID=%s, Source=%s).", error, artifactIDStr, sourceStr);
706  }
707 
719  private Optional<TimelineEvent> addOtherEventDesc(BlackboardArtifact artifact) throws TskCoreException, DuplicateException {
720  if (artifact == null) {
721  return Optional.empty();
722  }
723 
724  Long timeVal = artifact.getAttributes().stream()
725  .filter((attr) -> attr.getAttributeType().getValueType() == BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME)
726  .map(attr -> attr.getValueLong())
727  .findFirst()
728  .orElse(null);
729 
730  if (timeVal == null) {
731  return Optional.empty();
732  }
733 
734  String description = String.format("%s: %d", artifact.getDisplayName(), artifact.getId());
735 
736  TimelineEventDescriptionWithTime evtWDesc = new TimelineEventDescriptionWithTime(timeVal, description, description, description);
737 
738  TimelineEventType evtType = (ARTIFACT_TYPE_IDS.contains(artifact.getArtifactTypeID()))
739  ? TimelineEventType.OTHER
740  : TimelineEventType.USER_CREATED;
741 
742  return addArtifactEvent(evtWDesc, evtType, artifact);
743  }
744 
758  private Optional<TimelineEvent> addArtifactEvent(TimelineEventDescriptionWithTime eventPayload,
759  TimelineEventType eventType, BlackboardArtifact artifact) throws TskCoreException, DuplicateException {
760 
761  if (eventPayload == null) {
762  return Optional.empty();
763  }
764  long time = eventPayload.getTime();
765  // 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
766  if (time <= 0 || time >= MAX_TIMESTAMP_TO_ADD) {
767  if (time >= MAX_TIMESTAMP_TO_ADD) {
768  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()));
769  }
770  return Optional.empty();
771  }
772  String fullDescription = eventPayload.getDescription(TimelineLevelOfDetail.HIGH);
773  String medDescription = eventPayload.getDescription(TimelineLevelOfDetail.MEDIUM);
774  String shortDescription = eventPayload.getDescription(TimelineLevelOfDetail.LOW);
775  long artifactID = artifact.getArtifactID();
776  long fileObjId = artifact.getObjectID();
777  long dataSourceObjectID = artifact.getDataSourceObjectID();
778 
779  AbstractFile file = caseDB.getAbstractFileById(fileObjId);
780  boolean hasHashHits = false;
781  // file will be null if source was data source or some non-file
782  if (file != null) {
783  hasHashHits = isNotEmpty(file.getHashSetNames());
784  }
785  boolean tagged = isNotEmpty(caseDB.getBlackboardArtifactTagsByArtifact(artifact));
786 
787  TimelineEvent event;
789  try (CaseDbConnection connection = caseDB.getConnection();) {
790 
791  long descriptionID = addEventDescription(dataSourceObjectID, fileObjId, artifactID,
792  fullDescription, medDescription, shortDescription,
793  hasHashHits, tagged, connection);
794 
795  long eventID = addEventWithExistingDescription(time, eventType, descriptionID, connection);
796 
797  event = new TimelineEvent(eventID, dataSourceObjectID, fileObjId, artifactID,
798  time, eventType, fullDescription, medDescription, shortDescription,
799  hasHashHits, tagged);
800 
801  } finally {
803  }
804  return Optional.of(event);
805  }
806 
807  private long addEventWithExistingDescription(Long time, TimelineEventType type, long descriptionID, CaseDbConnection connection) throws TskCoreException, DuplicateException {
808  String tableValuesClause
809  = "tsk_events ( event_type_id, event_description_id , time) VALUES (?, ?, ?)";
810 
811  String insertEventSql = getSqlIgnoreConflict(tableValuesClause);
812 
814  try (PreparedStatement insertRowStmt = connection.prepareStatement(insertEventSql, Statement.RETURN_GENERATED_KEYS);) {
815  insertRowStmt.clearParameters();
816  insertRowStmt.setLong(1, type.getTypeID());
817  insertRowStmt.setLong(2, descriptionID);
818  insertRowStmt.setLong(3, time);
819  int row = insertRowStmt.executeUpdate();
820  // if no inserted rows, return null.
821  if (row < 1) {
822  throw new DuplicateException(String.format("An event already exists in the event table for this item [time: %s, type: %s, description: %d].",
823  time == null ? "<null>" : Long.toString(time),
824  type == null ? "<null>" : type.toString(),
825  descriptionID));
826  }
827 
828  try (ResultSet generatedKeys = insertRowStmt.getGeneratedKeys();) {
829  if (generatedKeys.next()) {
830  return generatedKeys.getLong(1);
831  } else {
832  throw new DuplicateException(String.format("An event already exists in the event table for this item [time: %s, type: %s, description: %d].",
833  time == null ? "<null>" : Long.toString(time),
834  type == null ? "<null>" : type.toString(),
835  descriptionID));
836  }
837  }
838  } catch (SQLException ex) {
839  throw new TskCoreException("Failed to insert event for existing description.", ex); // NON-NLS
840  } finally {
842  }
843  }
844 
845  private Map<Long, Long> getEventAndDescriptionIDs(CaseDbConnection conn, long contentObjID, boolean includeArtifacts) throws TskCoreException {
846  return getEventAndDescriptionIDsHelper(conn, contentObjID, (includeArtifacts ? "" : " AND artifact_id IS NULL"));
847  }
848 
849  private Map<Long, Long> getEventAndDescriptionIDs(CaseDbConnection conn, long contentObjID, Long artifactID) throws TskCoreException {
850  return getEventAndDescriptionIDsHelper(conn, contentObjID, " AND artifact_id = " + artifactID);
851  }
852 
853  private Map<Long, Long> getEventAndDescriptionIDsHelper(CaseDbConnection con, long fileObjID, String artifactClause) throws TskCoreException {
854  //map from event_id to the event_description_id for that event.
855  Map<Long, Long> eventIDToDescriptionIDs = new HashMap<>();
856  String sql = "SELECT event_id, tsk_events.event_description_id"
857  + " FROM tsk_events "
858  + " LEFT JOIN tsk_event_descriptions ON ( tsk_events.event_description_id = tsk_event_descriptions.event_description_id )"
859  + " WHERE content_obj_id = " + fileObjID
860  + artifactClause;
861  try (Statement selectStmt = con.createStatement(); ResultSet executeQuery = selectStmt.executeQuery(sql);) {
862  while (executeQuery.next()) {
863  eventIDToDescriptionIDs.put(executeQuery.getLong("event_id"), executeQuery.getLong("event_description_id")); //NON-NLS
864  }
865  } catch (SQLException ex) {
866  throw new TskCoreException("Error getting event description ids for object id = " + fileObjID, ex);
867  }
868  return eventIDToDescriptionIDs;
869  }
870 
887  @Beta
888  public Set<Long> updateEventsForContentTagAdded(Content content) throws TskCoreException {
890  try (CaseDbConnection conn = caseDB.getConnection()) {
891  Map<Long, Long> eventIDs = getEventAndDescriptionIDs(conn, content.getId(), false);
892  updateEventSourceTaggedFlag(conn, eventIDs.values(), 1);
893  return eventIDs.keySet();
894  } finally {
896  }
897  }
898 
916  @Beta
917  public Set<Long> updateEventsForContentTagDeleted(Content content) throws TskCoreException {
919  try (CaseDbConnection conn = caseDB.getConnection()) {
920  if (caseDB.getContentTagsByContent(content).isEmpty()) {
921  Map<Long, Long> eventIDs = getEventAndDescriptionIDs(conn, content.getId(), false);
922  updateEventSourceTaggedFlag(conn, eventIDs.values(), 0);
923  return eventIDs.keySet();
924  } else {
925  return Collections.emptySet();
926  }
927  } finally {
929  }
930  }
931 
943  public Set<Long> updateEventsForArtifactTagAdded(BlackboardArtifact artifact) throws TskCoreException {
945  try (CaseDbConnection conn = caseDB.getConnection()) {
946  Map<Long, Long> eventIDs = getEventAndDescriptionIDs(conn, artifact.getObjectID(), artifact.getArtifactID());
947  updateEventSourceTaggedFlag(conn, eventIDs.values(), 1);
948  return eventIDs.keySet();
949  } finally {
951  }
952  }
953 
966  public Set<Long> updateEventsForArtifactTagDeleted(BlackboardArtifact artifact) throws TskCoreException {
968  try (CaseDbConnection conn = caseDB.getConnection()) {
969  if (caseDB.getBlackboardArtifactTagsByArtifact(artifact).isEmpty()) {
970  Map<Long, Long> eventIDs = getEventAndDescriptionIDs(conn, artifact.getObjectID(), artifact.getArtifactID());
971  updateEventSourceTaggedFlag(conn, eventIDs.values(), 0);
972  return eventIDs.keySet();
973  } else {
974  return Collections.emptySet();
975  }
976  } finally {
978  }
979  }
980 
981  private void updateEventSourceTaggedFlag(CaseDbConnection conn, Collection<Long> eventDescriptionIDs, int flagValue) throws TskCoreException {
982  if (eventDescriptionIDs.isEmpty()) {
983  return;
984  }
985 
986  String sql = "UPDATE tsk_event_descriptions SET tagged = " + flagValue + " WHERE event_description_id IN (" + buildCSVString(eventDescriptionIDs) + ")"; //NON-NLS
987  try (Statement updateStatement = conn.createStatement()) {
988  updateStatement.executeUpdate(sql);
989  } catch (SQLException ex) {
990  throw new TskCoreException("Error marking content events tagged: " + sql, ex);//NON-NLS
991  }
992  }
993 
1008  public Set<Long> updateEventsForHashSetHit(Content content) throws TskCoreException {
1010  try (CaseDbConnection con = caseDB.getConnection(); Statement updateStatement = con.createStatement();) {
1011  Map<Long, Long> eventIDs = getEventAndDescriptionIDs(con, content.getId(), true);
1012  if (!eventIDs.isEmpty()) {
1013  String sql = "UPDATE tsk_event_descriptions SET hash_hit = 1" + " WHERE event_description_id IN (" + buildCSVString(eventIDs.values()) + ")"; //NON-NLS
1014  try {
1015  updateStatement.executeUpdate(sql); //NON-NLS
1016  return eventIDs.keySet();
1017  } catch (SQLException ex) {
1018  throw new TskCoreException("Error setting hash_hit of events.", ex);//NON-NLS
1019  }
1020  } else {
1021  return eventIDs.keySet();
1022  }
1023  } catch (SQLException ex) {
1024  throw new TskCoreException("Error setting hash_hit of events.", ex);//NON-NLS
1025  } finally {
1027  }
1028  }
1029 
1030  void rollBackTransaction(SleuthkitCase.CaseDbTransaction trans) throws TskCoreException {
1031  trans.rollback();
1032  }
1033 
1053  public Map<TimelineEventType, Long> countEventsByType(Long startTime, Long endTime, TimelineFilter.RootFilter filter, TimelineEventType.HierarchyLevel typeHierachyLevel) throws TskCoreException {
1054  long adjustedEndTime = Objects.equals(startTime, endTime) ? endTime + 1 : endTime;
1055  //do we want the base or subtype column of the databse
1056  String typeColumn = typeColumnHelper(TimelineEventType.HierarchyLevel.EVENT.equals(typeHierachyLevel));
1057 
1058  String queryString = "SELECT count(DISTINCT tsk_events.event_id) AS count, " + typeColumn//NON-NLS
1059  + " FROM " + getAugmentedEventsTablesSQL(filter)//NON-NLS
1060  + " WHERE time >= " + startTime + " AND time < " + adjustedEndTime + " AND " + getSQLWhere(filter) // NON-NLS
1061  + " GROUP BY " + typeColumn; // NON-NLS
1062 
1064  try (CaseDbConnection con = caseDB.getConnection();
1065  Statement stmt = con.createStatement();
1066  ResultSet results = stmt.executeQuery(queryString);) {
1067  Map<TimelineEventType, Long> typeMap = new HashMap<>();
1068  while (results.next()) {
1069  int eventTypeID = results.getInt(typeColumn);
1070  TimelineEventType eventType = getEventType(eventTypeID)
1071  .orElseThrow(() -> newEventTypeMappingException(eventTypeID));//NON-NLS
1072 
1073  typeMap.put(eventType, results.getLong("count")); // NON-NLS
1074  }
1075  return typeMap;
1076  } catch (SQLException ex) {
1077  throw new TskCoreException("Error getting count of events from db: " + queryString, ex); // NON-NLS
1078  } finally {
1080  }
1081  }
1082 
1083  private static TskCoreException newEventTypeMappingException(int eventTypeID) {
1084  return new TskCoreException("Error mapping event type id " + eventTypeID + " to EventType.");//NON-NLS
1085  }
1086 
1100  static private String getAugmentedEventsTablesSQL(TimelineFilter.RootFilter filter) {
1101  TimelineFilter.FileTypesFilter fileTypesFitler = filter.getFileTypesFilter();
1102  boolean needsMimeTypes = fileTypesFitler != null && fileTypesFitler.hasSubFilters();
1103 
1104  return getAugmentedEventsTablesSQL(needsMimeTypes);
1105  }
1106 
1121  static private String getAugmentedEventsTablesSQL(boolean needMimeTypes) {
1122  /*
1123  * Regarding the timeline event tables schema, note that several columns
1124  * in the tsk_event_descriptions table seem, at first glance, to be
1125  * attributes of events rather than their descriptions and would appear
1126  * to belong in tsk_events table instead. The rationale for putting the
1127  * data source object ID, content object ID, artifact ID and the flags
1128  * indicating whether or not the event source has a hash set hit or is
1129  * tagged were motivated by the fact that these attributes are identical
1130  * for each event in a set of file system file MAC time events. The
1131  * decision was made to avoid duplication and save space by placing this
1132  * data in the tsk_event-descriptions table.
1133  */
1134  return "( SELECT event_id, time, tsk_event_descriptions.data_source_obj_id, content_obj_id, artifact_id, "
1135  + " full_description, med_description, short_description, tsk_events.event_type_id, super_type_id,"
1136  + " hash_hit, tagged "
1137  + (needMimeTypes ? ", mime_type" : "")
1138  + " FROM tsk_events "
1139  + " JOIN tsk_event_descriptions ON ( tsk_event_descriptions.event_description_id = tsk_events.event_description_id)"
1140  + " JOIN tsk_event_types ON (tsk_events.event_type_id = tsk_event_types.event_type_id ) "
1141  + (needMimeTypes ? " LEFT OUTER JOIN tsk_files "
1142  + " ON (tsk_event_descriptions.content_obj_id = tsk_files.obj_id)"
1143  : "")
1144  + ") AS tsk_events";
1145  }
1146 
1154  private static int booleanToInt(boolean value) {
1155  return value ? 1 : 0;
1156  }
1157 
1158  private static boolean intToBoolean(int value) {
1159  return value != 0;
1160  }
1161 
1174  public List<TimelineEvent> getEvents(Interval timeRange, TimelineFilter.RootFilter filter) throws TskCoreException {
1175  List<TimelineEvent> events = new ArrayList<>();
1176 
1177  Long startTime = timeRange.getStartMillis() / 1000;
1178  Long endTime = timeRange.getEndMillis() / 1000;
1179 
1180  if (Objects.equals(startTime, endTime)) {
1181  endTime++; //make sure end is at least 1 millisecond after start
1182  }
1183 
1184  if (filter == null) {
1185  return events;
1186  }
1187 
1188  if (endTime < startTime) {
1189  return events;
1190  }
1191 
1192  //build dynamic parts of query
1193  String querySql = "SELECT time, content_obj_id, data_source_obj_id, artifact_id, " // NON-NLS
1194  + " event_id, " //NON-NLS
1195  + " hash_hit, " //NON-NLS
1196  + " tagged, " //NON-NLS
1197  + " event_type_id, super_type_id, "
1198  + " full_description, med_description, short_description " // NON-NLS
1199  + " FROM " + getAugmentedEventsTablesSQL(filter) // NON-NLS
1200  + " WHERE time >= " + startTime + " AND time < " + endTime + " AND " + getSQLWhere(filter) // NON-NLS
1201  + " ORDER BY time"; // NON-NLS
1202 
1204  try (CaseDbConnection con = caseDB.getConnection();
1205  Statement stmt = con.createStatement();
1206  ResultSet resultSet = stmt.executeQuery(querySql);) {
1207 
1208  while (resultSet.next()) {
1209  int eventTypeID = resultSet.getInt("event_type_id");
1210  TimelineEventType eventType = getEventType(eventTypeID).orElseThrow(()
1211  -> new TskCoreException("Error mapping event type id " + eventTypeID + "to EventType."));//NON-NLS
1212 
1213  TimelineEvent event = new TimelineEvent(
1214  resultSet.getLong("event_id"), // NON-NLS
1215  resultSet.getLong("data_source_obj_id"), // NON-NLS
1216  resultSet.getLong("content_obj_id"), // NON-NLS
1217  resultSet.getLong("artifact_id"), // NON-NLS
1218  resultSet.getLong("time"), // NON-NLS
1219  eventType,
1220  resultSet.getString("full_description"), // NON-NLS
1221  resultSet.getString("med_description"), // NON-NLS
1222  resultSet.getString("short_description"), // NON-NLS
1223  resultSet.getInt("hash_hit") != 0, //NON-NLS
1224  resultSet.getInt("tagged") != 0);
1225 
1226  events.add(event);
1227  }
1228 
1229  } catch (SQLException ex) {
1230  throw new TskCoreException("Error getting events from db: " + querySql, ex); // NON-NLS
1231  } finally {
1233  }
1234 
1235  return events;
1236  }
1237 
1245  private static String typeColumnHelper(final boolean useSubTypes) {
1246  return useSubTypes ? "event_type_id" : "super_type_id"; //NON-NLS
1247  }
1248 
1257  String getSQLWhere(TimelineFilter.RootFilter filter) {
1258 
1259  String result;
1260  if (filter == null) {
1261  return getTrueLiteral();
1262  } else {
1263  result = filter.getSQLWhere(this);
1264  }
1265 
1266  return result;
1267  }
1268 
1280  private String getSqlIgnoreConflict(String insertTableValues) throws TskCoreException {
1281  switch (caseDB.getDatabaseType()) {
1282  case POSTGRESQL:
1283  return "INSERT INTO " + insertTableValues + " ON CONFLICT DO NOTHING";
1284  case SQLITE:
1285  return "INSERT OR IGNORE INTO " + insertTableValues;
1286  default:
1287  throw new TskCoreException("Unknown DB Type: " + caseDB.getDatabaseType().name());
1288  }
1289  }
1290 
1291  private String getTrueLiteral() {
1292  switch (caseDB.getDatabaseType()) {
1293  case POSTGRESQL:
1294  return "TRUE";//NON-NLS
1295  case SQLITE:
1296  return "1";//NON-NLS
1297  default:
1298  throw new UnsupportedOperationException("Unsupported DB type: " + caseDB.getDatabaseType().name());//NON-NLS
1299 
1300  }
1301  }
1302 
1307  final static public class TimelineEventAddedEvent {
1308 
1309  private final TimelineEvent addedEvent;
1310 
1312  return addedEvent;
1313  }
1314 
1316  this.addedEvent = event;
1317  }
1318  }
1319 
1323  private static class DuplicateException extends Exception {
1324 
1325  private static final long serialVersionUID = 1L;
1326 
1332  DuplicateException(String message) {
1333  super(message);
1334  }
1335  }
1336 }
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-2020 Brian Carrier. (carrier -at- sleuthkit -dot- org)
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.