/* * Autopsy Forensic Browser * * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sleuthkit.autopsy.timeline.datamodel; import com.google.common.eventbus.EventBus; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import javafx.beans.Observable; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.collections.ListChangeListener; import javafx.collections.MapChangeListener; import javax.annotation.concurrent.GuardedBy; import org.joda.time.Interval; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagDeletedEvent; import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagDeletedEvent.DeletedBlackboardArtifactTagInfo; import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent.DeletedContentTagInfo; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType; import org.sleuthkit.autopsy.timeline.db.EventsRepository; import org.sleuthkit.autopsy.timeline.events.DBUpdatedEvent; import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent; import org.sleuthkit.autopsy.timeline.events.TagsAddedEvent; import org.sleuthkit.autopsy.timeline.events.TagsDeletedEvent; import org.sleuthkit.autopsy.timeline.filters.DataSourceFilter; import org.sleuthkit.autopsy.timeline.filters.DataSourcesFilter; import org.sleuthkit.autopsy.timeline.filters.Filter; import org.sleuthkit.autopsy.timeline.filters.HashHitsFilter; import org.sleuthkit.autopsy.timeline.filters.HashSetFilter; import org.sleuthkit.autopsy.timeline.filters.HideKnownFilter; import org.sleuthkit.autopsy.timeline.filters.RootFilter; import org.sleuthkit.autopsy.timeline.filters.TagNameFilter; import org.sleuthkit.autopsy.timeline.filters.TagsFilter; import org.sleuthkit.autopsy.timeline.filters.TextFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifactTag; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; /** * This class acts as the model for a TimelineView * * Views can register listeners on properties returned by methods. * * This class is implemented as a filtered view into an underlying * EventsRepository. * * TODO: as many methods as possible should cache their results so as to avoid * unnecessary db calls through the EventsRepository -jm * * Concurrency Policy: repo is internally synchronized, so methods that only * access the repo atomically do not need further synchronization * * all other member state variables should only be accessed with intrinsic lock * of containing FilteredEventsModel held. Many methods delegate to a task * submitted to the dbQueryThread executor. These methods should synchronize on * this object, and the tasks should too. Since the tasks execute asynchronously * from the invoking methods, the methods will return and release the lock for * the tasks to obtain. * */ public final class FilteredEventsModel { private static final Logger LOGGER = Logger.getLogger(FilteredEventsModel.class.getName()); /** * time range that spans the filtered events */ @GuardedBy("this") private final ReadOnlyObjectWrapper<Interval> requestedTimeRange = new ReadOnlyObjectWrapper<>(); @GuardedBy("this") private final ReadOnlyObjectWrapper<RootFilter> requestedFilter = new ReadOnlyObjectWrapper<>(); @GuardedBy("this") private final ReadOnlyObjectWrapper< EventTypeZoomLevel> requestedTypeZoom = new ReadOnlyObjectWrapper<>(EventTypeZoomLevel.BASE_TYPE); @GuardedBy("this") private final ReadOnlyObjectWrapper< DescriptionLoD> requestedLOD = new ReadOnlyObjectWrapper<>(DescriptionLoD.SHORT); @GuardedBy("this") private final ReadOnlyObjectWrapper<ZoomParams> requestedZoomParamters = new ReadOnlyObjectWrapper<>(); private final EventBus eventbus = new EventBus("FilteredEventsModel_EventBus"); //NON-NLS /** * The underlying repo for events. Atomic access to repo is synchronized * internally, but compound access should be done with the intrinsic lock of * this FilteredEventsModel object */ @GuardedBy("this") private final EventsRepository repo; private final Case autoCase; public FilteredEventsModel(EventsRepository repo, ReadOnlyObjectProperty<ZoomParams> currentStateProperty) { this.repo = repo; this.autoCase = repo.getAutoCase(); repo.getDatasourcesMap().addListener((MapChangeListener.Change<? extends Long, ? extends String> change) -> { DataSourceFilter dataSourceFilter = new DataSourceFilter(change.getValueAdded(), change.getKey()); RootFilter rootFilter = filterProperty().get(); rootFilter.getDataSourcesFilter().addSubFilter(dataSourceFilter); requestedFilter.set(rootFilter.copyOf()); }); repo.getHashSetMap().addListener((MapChangeListener.Change<? extends Long, ? extends String> change) -> { HashSetFilter hashSetFilter = new HashSetFilter(change.getValueAdded(), change.getKey()); RootFilter rootFilter = filterProperty().get(); rootFilter.getHashHitsFilter().addSubFilter(hashSetFilter); requestedFilter.set(rootFilter.copyOf()); }); repo.getTagNames().addListener((ListChangeListener.Change<? extends TagName> c) -> { RootFilter rootFilter = filterProperty().get(); TagsFilter tagsFilter = rootFilter.getTagsFilter(); repo.syncTagsFilter(tagsFilter); requestedFilter.set(rootFilter.copyOf()); }); requestedFilter.set(getDefaultFilter()); //TODO: use bindings to keep these in sync? -jm requestedZoomParamters.addListener((Observable observable) -> { final ZoomParams zoomParams = requestedZoomParamters.get(); if (zoomParams != null) { synchronized (FilteredEventsModel.this) { requestedTypeZoom.set(zoomParams.getTypeZoomLevel()); requestedFilter.set(zoomParams.getFilter()); requestedTimeRange.set(zoomParams.getTimeRange()); requestedLOD.set(zoomParams.getDescriptionLOD()); } } }); requestedZoomParamters.bind(currentStateProperty); } /** * Readonly observable property for the current ZoomParams * * @return A readonly observable property for the current ZoomParams. */ synchronized public ReadOnlyObjectProperty<ZoomParams> zoomParametersProperty() { return requestedZoomParamters.getReadOnlyProperty(); } /** * Get the current ZoomParams * * @return The current ZoomParams */ synchronized public ZoomParams getZoomParamaters() { return requestedZoomParamters.get(); } /** * Get a read only view of the time range currently in view. * * @return A read only view of the time range currently in view. */ synchronized public ReadOnlyObjectProperty<Interval> timeRangeProperty() { if (requestedTimeRange.get() == null) { requestedTimeRange.set(getSpanningInterval()); } return requestedTimeRange.getReadOnlyProperty(); } synchronized public ReadOnlyObjectProperty<DescriptionLoD> descriptionLODProperty() { return requestedLOD.getReadOnlyProperty(); } synchronized public ReadOnlyObjectProperty<RootFilter> filterProperty() { return requestedFilter.getReadOnlyProperty(); } synchronized public ReadOnlyObjectProperty<EventTypeZoomLevel> eventTypeZoomProperty() { return requestedTypeZoom.getReadOnlyProperty(); } /** * The time range currently in view. * * @return The time range currently in view. */ synchronized public Interval getTimeRange() { return timeRangeProperty().get(); } synchronized public DescriptionLoD getDescriptionLOD() { return requestedLOD.get(); } synchronized public RootFilter getFilter() { return requestedFilter.get(); } synchronized public EventTypeZoomLevel getEventTypeZoom() { return requestedTypeZoom.get(); } /** * @return the default filter used at startup */ public RootFilter getDefaultFilter() { DataSourcesFilter dataSourcesFilter = new DataSourcesFilter(); repo.getDatasourcesMap().entrySet().stream().forEach((Map.Entry<Long, String> t) -> { DataSourceFilter dataSourceFilter = new DataSourceFilter(t.getValue(), t.getKey()); dataSourceFilter.setSelected(Boolean.TRUE); dataSourcesFilter.addSubFilter(dataSourceFilter); }); HashHitsFilter hashHitsFilter = new HashHitsFilter(); repo.getHashSetMap().entrySet().stream().forEach((Map.Entry<Long, String> t) -> { HashSetFilter hashSetFilter = new HashSetFilter(t.getValue(), t.getKey()); hashSetFilter.setSelected(Boolean.TRUE); hashHitsFilter.addSubFilter(hashSetFilter); }); TagsFilter tagsFilter = new TagsFilter(); repo.getTagNames().stream().forEach(t -> { TagNameFilter tagNameFilter = new TagNameFilter(t, autoCase); tagNameFilter.setSelected(Boolean.TRUE); tagsFilter.addSubFilter(tagNameFilter); }); return new RootFilter(new HideKnownFilter(), tagsFilter, hashHitsFilter, new TextFilter(), new TypeFilter(RootEventType.getInstance()), dataSourcesFilter, Collections.emptySet()); } public Interval getBoundingEventsInterval() { return repo.getBoundingEventsInterval(zoomParametersProperty().get().getTimeRange(), zoomParametersProperty().get().getFilter()); } public SingleEvent getEventById(Long eventID) { return repo.getEventById(eventID); } public Set<SingleEvent> getEventsById(Collection<Long> eventIDs) { return repo.getEventsById(eventIDs); } /** * get a count of tagnames applied to the given event ids as a map from * tagname displayname to count of tag applications * * @param eventIDsWithTags the event ids to get the tag counts map for * * @return a map from tagname displayname to count of applications */ public Map<String, Long> getTagCountsByTagName(Set<Long> eventIDsWithTags) { return repo.getTagCountsByTagName(eventIDsWithTags); } public List<Long> getEventIDs(Interval timeRange, Filter filter) { final Interval overlap; final RootFilter intersect; synchronized (this) { overlap = getSpanningInterval().overlap(timeRange); intersect = requestedFilter.get().copyOf(); } intersect.getSubFilters().add(filter); return repo.getEventIDs(overlap, intersect); } /** * Get a representation of all the events, within the given time range, that * pass the given filter, grouped by time and description such that file * system events for the same file, with the same timestamp, are combined * together. * * @return A List of combined events, sorted by timestamp. */ public List<CombinedEvent> getCombinedEvents() { return repo.getCombinedEvents(requestedTimeRange.get(), requestedFilter.get()); } /** * return the number of events that pass the requested filter and are within * the given time range. * * NOTE: this method does not change the requested time range * * @param timeRange * * @return */ public Map<EventType, Long> getEventCounts(Interval timeRange) { final RootFilter filter; final EventTypeZoomLevel typeZoom; synchronized (this) { filter = requestedFilter.get(); typeZoom = requestedTypeZoom.get(); } return repo.countEvents(new ZoomParams(timeRange, typeZoom, filter, null)); } /** * @return the smallest interval spanning all the events from the * repository, ignoring any filters or requested ranges */ public Interval getSpanningInterval() { return new Interval(getMinTime() * 1000, 1000 + getMaxTime() * 1000); } /** * @return the smallest interval spanning all the given events */ public Interval getSpanningInterval(Collection<Long> eventIDs) { return repo.getSpanningInterval(eventIDs); } /** * @return the time (in seconds from unix epoch) of the absolutely first * event available from the repository, ignoring any filters or * requested ranges */ public Long getMinTime() { return repo.getMinTime(); } /** * @return the time (in seconds from unix epoch) of the absolutely last * event available from the repository, ignoring any filters or * requested ranges */ public Long getMaxTime() { return repo.getMaxTime(); } /** * * @return a list of event clusters at the requested zoom levels that are * within the requested time range and pass the requested filter */ public List<EventStripe> getEventStripes() { final Interval range; final RootFilter filter; final EventTypeZoomLevel zoom; final DescriptionLoD lod; synchronized (this) { range = requestedTimeRange.get(); filter = requestedFilter.get(); zoom = requestedTypeZoom.get(); lod = requestedLOD.get(); } return repo.getEventStripes(new ZoomParams(range, zoom, filter, lod)); } /** * @param params * * @return a list of aggregated events that are within the requested time * range and pass the requested filter, using the given aggregation * to control the grouping of events */ public List<EventStripe> getEventStripes(ZoomParams params) { return repo.getEventStripes(params); } synchronized public boolean handleContentTagAdded(ContentTagAddedEvent evt) { ContentTag contentTag = evt.getAddedTag(); Content content = contentTag.getContent(); Set<Long> updatedEventIDs = repo.addTag(content.getId(), null, contentTag, null); return postTagsAdded(updatedEventIDs); } synchronized public boolean handleArtifactTagAdded(BlackBoardArtifactTagAddedEvent evt) { BlackboardArtifactTag artifactTag = evt.getAddedTag(); BlackboardArtifact artifact = artifactTag.getArtifact(); Set<Long> updatedEventIDs = repo.addTag(artifact.getObjectID(), artifact.getArtifactID(), artifactTag, null); return postTagsAdded(updatedEventIDs); } synchronized public boolean handleContentTagDeleted(ContentTagDeletedEvent evt) { DeletedContentTagInfo deletedTagInfo = evt.getDeletedTagInfo(); try { Content content = autoCase.getSleuthkitCase().getContentById(deletedTagInfo.getContentID()); boolean tagged = autoCase.getServices().getTagsManager().getContentTagsByContent(content).isEmpty() == false; Set<Long> updatedEventIDs = repo.deleteTag(content.getId(), null, deletedTagInfo.getTagID(), tagged); return postTagsDeleted(updatedEventIDs); } catch (TskCoreException ex) { LOGGER.log(Level.SEVERE, "unable to determine tagged status of content.", ex); //NON-NLS } return false; } synchronized public boolean handleArtifactTagDeleted(BlackBoardArtifactTagDeletedEvent evt) { DeletedBlackboardArtifactTagInfo deletedTagInfo = evt.getDeletedTagInfo(); try { BlackboardArtifact artifact = autoCase.getSleuthkitCase().getBlackboardArtifact(deletedTagInfo.getArtifactID()); boolean tagged = autoCase.getServices().getTagsManager().getBlackboardArtifactTagsByArtifact(artifact).isEmpty() == false; Set<Long> updatedEventIDs = repo.deleteTag(artifact.getObjectID(), artifact.getArtifactID(), deletedTagInfo.getTagID(), tagged); return postTagsDeleted(updatedEventIDs); } catch (TskCoreException ex) { LOGGER.log(Level.SEVERE, "unable to determine tagged status of artifact.", ex); //NON-NLS } return false; } /** * Get a List of event IDs for the events that are derived from the given * file. * * @param file The AbstractFile to get derived event IDs * for. * @param includeDerivedArtifacts If true, also get event IDs for events * derived from artifacts derived form this * file. If false, only gets events derived * directly from this file (file system * timestamps). * * @return A List of event IDs for the events that are derived from the * given file. */ public List<Long> getEventIDsForFile(AbstractFile file, boolean includeDerivedArtifacts) { return repo.getEventIDsForFile(file, includeDerivedArtifacts); } /** * Get a List of event IDs for the events that are derived from the given * artifact. * * @param artifact The BlackboardArtifact to get derived event IDs for. * * @return A List of event IDs for the events that are derived from the * given artifact. */ public List<Long> getEventIDsForArtifact(BlackboardArtifact artifact) { return repo.getEventIDsForArtifact(artifact); } /** * Post a TagsAddedEvent to all registered subscribers, if the given set of * updated event IDs is not empty. * * @param updatedEventIDs The set of event ids to be included in the * TagsAddedEvent. * * @return True if an event was posted. */ private boolean postTagsAdded(Set<Long> updatedEventIDs) { boolean tagsUpdated = !updatedEventIDs.isEmpty(); if (tagsUpdated) { eventbus.post(new TagsAddedEvent(updatedEventIDs)); } return tagsUpdated; } /** * Post a TagsDeletedEvent to all registered subscribers, if the given set * of updated event IDs is not empty. * * @param updatedEventIDs The set of event ids to be included in the * TagsDeletedEvent. * * @return True if an event was posted. */ private boolean postTagsDeleted(Set<Long> updatedEventIDs) { boolean tagsUpdated = !updatedEventIDs.isEmpty(); if (tagsUpdated) { eventbus.post(new TagsDeletedEvent(updatedEventIDs)); } return tagsUpdated; } /** * Register the given object to receive events. * * @param o The object to register. Must implement public methods annotated * with Subscribe. */ synchronized public void registerForEvents(Object o) { eventbus.register(o); } /** * Un-register the given object, so it no longer receives events. * * @param o The object to un-register. */ synchronized public void unRegisterForEvents(Object o) { eventbus.unregister(0); } /** * Post a DBUpdatedEvent to all registered subscribers. */ public void postDBUpdated() { eventbus.post(new DBUpdatedEvent()); } /** * Post a RefreshRequestedEvent to all registered subscribers. */ public void postRefreshRequest() { eventbus.post(new RefreshRequestedEvent()); } /** * (Re)Post an AutopsyEvent received from another event distribution system * locally to all registered subscribers. */ public void postAutopsyEventLocally(AutopsyEvent event) { eventbus.post(event); } }