mirror of
https://github.com/overcuriousity/autopsy-flatpak.git
synced 2025-07-12 16:06:15 +00:00
431 lines
21 KiB
Java
431 lines
21 KiB
Java
/*
|
|
* Autopsy Forensic Browser
|
|
*
|
|
* Copyright 2013-2018 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.imagegallery;
|
|
|
|
import java.beans.PropertyChangeEvent;
|
|
import java.beans.PropertyChangeListener;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.Paths;
|
|
import java.util.EnumSet;
|
|
import java.util.Set;
|
|
import java.util.logging.Level;
|
|
import javafx.application.Platform;
|
|
import javax.annotation.Nonnull;
|
|
import javax.annotation.concurrent.GuardedBy;
|
|
import javax.swing.JOptionPane;
|
|
import javax.swing.SwingUtilities;
|
|
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
|
import org.openide.util.NbBundle;
|
|
import org.sleuthkit.autopsy.casemodule.Case;
|
|
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
|
|
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
|
|
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
|
|
import org.sleuthkit.autopsy.core.RuntimeProperties;
|
|
import org.sleuthkit.autopsy.coreutils.Logger;
|
|
import org.sleuthkit.autopsy.events.AutopsyEvent;
|
|
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB;
|
|
import org.sleuthkit.autopsy.ingest.IngestManager;
|
|
import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.DATA_ADDED;
|
|
import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.FILE_DONE;
|
|
import org.sleuthkit.autopsy.ingest.ModuleDataEvent;
|
|
import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisEvent;
|
|
import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector;
|
|
import org.sleuthkit.datamodel.AbstractFile;
|
|
import org.sleuthkit.datamodel.BlackboardArtifact;
|
|
import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
|
|
import org.sleuthkit.datamodel.Content;
|
|
import org.sleuthkit.datamodel.TskCoreException;
|
|
import org.sleuthkit.datamodel.TskData;
|
|
|
|
/**
|
|
* This class is reponsible for handling selected application events for the
|
|
* image gallery module, managing the image gallery module's per case MVC
|
|
* controller and keeping track of the following state: the module name, the
|
|
* module output directory and whether or not the ingest gallery module is
|
|
* enabled for the current case.
|
|
*/
|
|
@NbBundle.Messages({"ImageGalleryModule.moduleName=Image Gallery"})
|
|
public class ImageGalleryModule {
|
|
|
|
private static final Logger logger = Logger.getLogger(ImageGalleryModule.class.getName());
|
|
private static final String MODULE_NAME = Bundle.ImageGalleryModule_moduleName();
|
|
private static final Set<Case.Events> CASE_EVENTS_OF_INTEREST = EnumSet.of(
|
|
Case.Events.CURRENT_CASE,
|
|
Case.Events.DATA_SOURCE_ADDED,
|
|
Case.Events.CONTENT_TAG_ADDED,
|
|
Case.Events.CONTENT_TAG_DELETED
|
|
);
|
|
private static final Object controllerLock = new Object();
|
|
@GuardedBy("controllerLock")
|
|
private static ImageGalleryController controller;
|
|
|
|
/**
|
|
* Gets the per case image gallery controller for the current case. The
|
|
* controller is changed in the case event listener.
|
|
*
|
|
* @return The image gallery controller for the current case.
|
|
*
|
|
* @throws TskCoreException If there is a problem creating the controller.
|
|
*/
|
|
public static ImageGalleryController getController() throws TskCoreException {
|
|
synchronized (controllerLock) {
|
|
if (controller == null) {
|
|
try {
|
|
Case currentCase = Case.getCurrentCaseThrows();
|
|
controller = new ImageGalleryController(currentCase);
|
|
} catch (NoCurrentCaseException ex) {
|
|
throw new TskCoreException("Failed to get ", ex);
|
|
}
|
|
}
|
|
return controller;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the implicit exit property attribute of the JavaFX runtime to false
|
|
* and sets up listeners for application events. It is invoked at
|
|
* application start up by virtue of the OnStart annotation on the OnStart
|
|
* class in this package.
|
|
*/
|
|
static void onStart() {
|
|
Platform.setImplicitExit(false);
|
|
IngestManager.getInstance().addIngestJobEventListener(new IngestJobEventListener());
|
|
IngestManager.getInstance().addIngestModuleEventListener(new IngestModuleEventListener());
|
|
Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, new CaseEventListener());
|
|
}
|
|
|
|
/**
|
|
* Gets the image gallery module name.
|
|
*
|
|
* @return The module name,
|
|
*/
|
|
static String getModuleName() {
|
|
return MODULE_NAME;
|
|
}
|
|
|
|
/**
|
|
* Gets the path to the image gallery module output folder for a given case.
|
|
*
|
|
* @param theCase The case.
|
|
*
|
|
* @return The path to the image gallery module output folder for the case.
|
|
*/
|
|
public static Path getModuleOutputDir(Case theCase) {
|
|
return Paths.get(theCase.getModuleDirectory(), getModuleName());
|
|
}
|
|
|
|
/**
|
|
* Prevents instantiation.
|
|
*/
|
|
private ImageGalleryModule() {
|
|
}
|
|
|
|
/**
|
|
* Indicates whether or not the image gallery module is enabled for a given
|
|
* case.
|
|
*
|
|
* @param theCase The case.
|
|
*
|
|
* @return True or false.
|
|
*/
|
|
static boolean isEnabledforCase(@Nonnull Case theCase) {
|
|
String enabledforCaseProp = new PerCaseProperties(theCase).getConfigSetting(ImageGalleryModule.MODULE_NAME, PerCaseProperties.ENABLED);
|
|
return isNotBlank(enabledforCaseProp) ? Boolean.valueOf(enabledforCaseProp) : ImageGalleryPreferences.isEnabledByDefault();
|
|
}
|
|
|
|
/**
|
|
* Indicates whether or not a given file is of interest to the image gallery
|
|
* module (is "drawable") and is not marked as a "known" file (e.g., is not
|
|
* a file in the NSRL hash set).
|
|
*
|
|
* @param file The file.
|
|
*
|
|
* @return True if the file is "drawable" and not "known", false otherwise.
|
|
*
|
|
* @throws FileTypeDetectorInitException If there is an error determining
|
|
* the type of the file.
|
|
*/
|
|
private static boolean isDrawableAndNotKnown(AbstractFile abstractFile) throws FileTypeDetector.FileTypeDetectorInitException {
|
|
return (abstractFile.getKnown() != TskData.FileKnown.KNOWN) && FileTypeUtils.isDrawable(abstractFile);
|
|
}
|
|
|
|
/**
|
|
* A listener for ingest module application events.
|
|
*/
|
|
static private class IngestModuleEventListener implements PropertyChangeListener {
|
|
|
|
@Override
|
|
public void propertyChange(PropertyChangeEvent event) {
|
|
/*
|
|
* Only process individual files and artifacts in "real time" on the
|
|
* node that is running the ingest job. On a remote node, image
|
|
* files are processed as a group when the ingest job is complete.
|
|
*/
|
|
// RJCTODO: DO we need to handle any events at all on an auot ingest node?
|
|
if (((AutopsyEvent) event).getSourceType() != AutopsyEvent.SourceType.LOCAL) {
|
|
return;
|
|
}
|
|
|
|
ImageGalleryController currentController;
|
|
try {
|
|
currentController = getController();
|
|
// RJCTODO: If a closed controller had a method that could be
|
|
// queried to determine whether it was shut down, we could
|
|
// bail out here. The older code that used to try to check for
|
|
// a current case was flawed; there was no guarantee the current
|
|
// case was the same case associated with the event.
|
|
} catch (TskCoreException ex) {
|
|
logger.log(Level.SEVERE, String.format("Failed to handle %s event", event.getPropertyName()), ex); //NON-NLS
|
|
return;
|
|
}
|
|
|
|
String eventType = event.getPropertyName();
|
|
switch (IngestManager.IngestModuleEvent.valueOf(eventType)) {
|
|
case FILE_DONE:
|
|
AbstractFile file = (AbstractFile) event.getNewValue();
|
|
if (!file.isFile()) {
|
|
return;
|
|
}
|
|
if (currentController.isListeningEnabled()) {
|
|
try {
|
|
if (isDrawableAndNotKnown(file)) {
|
|
currentController.queueDBTask(new ImageGalleryController.UpdateFileTask(file, currentController.getDatabase()));
|
|
}
|
|
} catch (FileTypeDetector.FileTypeDetectorInitException ex) {
|
|
logger.log(Level.SEVERE, String.format("Failed to determine if file is of interest to the image gallery module, ignoring file (obj_id=%d)", file.getId()), ex); //NON-NLS
|
|
}
|
|
}
|
|
break;
|
|
case DATA_ADDED:
|
|
ModuleDataEvent artifactAddedEvent = (ModuleDataEvent) event.getOldValue();
|
|
if (artifactAddedEvent.getBlackboardArtifactType().getTypeID() == ARTIFACT_TYPE.TSK_METADATA_EXIF.getTypeID()) {
|
|
DrawableDB drawableDB = currentController.getDatabase();
|
|
if (artifactAddedEvent.getArtifacts() != null) {
|
|
for (BlackboardArtifact art : artifactAddedEvent.getArtifacts()) {
|
|
drawableDB.addExifCache(art.getObjectID());
|
|
}
|
|
}
|
|
} else if (artifactAddedEvent.getBlackboardArtifactType().getTypeID() == ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID()) {
|
|
DrawableDB drawableDB = currentController.getDatabase();
|
|
if (artifactAddedEvent.getArtifacts() != null) {
|
|
for (BlackboardArtifact art : artifactAddedEvent.getArtifacts()) {
|
|
drawableDB.addHashSetCache(art.getObjectID());
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A listener for case application events.
|
|
*/
|
|
// RJCTODO: This code would be easier to read if there were two case event
|
|
// listeners, one that handled CURRENT_CASE events and one that handled
|
|
// the other events. Or event better, move the handling of Case events other
|
|
// than CURRENT_CASE into ImageGalleryController.
|
|
static private class CaseEventListener implements PropertyChangeListener {
|
|
|
|
@Override
|
|
public void propertyChange(PropertyChangeEvent event) {
|
|
// RJCTODO: DO we need to handle any events at all on an auot ingest node?
|
|
Case.Events eventType = Case.Events.valueOf(event.getPropertyName());
|
|
if (eventType == Case.Events.CURRENT_CASE) {
|
|
synchronized (controllerLock) {
|
|
if (event.getNewValue() != null) {
|
|
/*
|
|
* CURRENT_CASE(_OPENED) event.
|
|
*/
|
|
Case newCase = (Case) event.getNewValue();
|
|
try {
|
|
controller = new ImageGalleryController(newCase);
|
|
} catch (TskCoreException ex) {
|
|
logger.log(Level.SEVERE, String.format("Failed to construct controller for new case %s (%s)", newCase.getDisplayName(), newCase.getName()), ex);
|
|
}
|
|
} else if (event.getOldValue() != null) {
|
|
/*
|
|
* CURRENT_CASE(_CLOSED) event.
|
|
*/
|
|
SwingUtilities.invokeLater(ImageGalleryTopComponent::closeTopComponent);
|
|
controller.shutDown();
|
|
}
|
|
}
|
|
} else {
|
|
ImageGalleryController currentController;
|
|
try {
|
|
currentController = getController();
|
|
// RJCTODO: I think it would be best to move handling of these
|
|
// case events into the controller class and have the controller
|
|
// instance register/unregister as a listener when it is
|
|
// contructed and shuts down. This will improve the encapsulation
|
|
// of ImageGalleryController and allow it to check its own open/closed
|
|
// state before handling an event.
|
|
} catch (TskCoreException ex) {
|
|
logger.log(Level.SEVERE, String.format("Failed to handle %s event", event.getPropertyName()), ex); //NON-NLS
|
|
return;
|
|
}
|
|
|
|
switch (eventType) {
|
|
case DATA_SOURCE_ADDED:
|
|
if (((AutopsyEvent) event).getSourceType() == AutopsyEvent.SourceType.LOCAL) {
|
|
Content newDataSource = (Content) event.getNewValue();
|
|
if (currentController.isListeningEnabled()) {
|
|
currentController.getDatabase().insertOrUpdateDataSource(newDataSource.getId(), DrawableDB.DrawableDbBuildStatusEnum.UNKNOWN);
|
|
}
|
|
}
|
|
break;
|
|
case CONTENT_TAG_ADDED:
|
|
final ContentTagAddedEvent tagAddedEvent = (ContentTagAddedEvent) event;
|
|
long objId = tagAddedEvent.getAddedTag().getContent().getId();
|
|
DrawableDB drawableDB = currentController.getDatabase();
|
|
drawableDB.addTagCache(objId); // RJCTODO: Why add the tag to the cache before doing the in DB check?
|
|
if (drawableDB.isInDB(objId)) {
|
|
currentController.getTagsManager().fireTagAddedEvent(tagAddedEvent);
|
|
}
|
|
break;
|
|
case CONTENT_TAG_DELETED:
|
|
final ContentTagDeletedEvent tagDeletedEvent = (ContentTagDeletedEvent) event;
|
|
if (currentController.getDatabase().isInDB(tagDeletedEvent.getDeletedTagInfo().getContentID())) {
|
|
currentController.getTagsManager().fireTagDeletedEvent(tagDeletedEvent);
|
|
} // RJCTODO: Why not remove the tag from the cache?
|
|
break;
|
|
default:
|
|
logger.log(Level.SEVERE, String.format("Received %s event with no subscription", event.getPropertyName())); //NON-NLS
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A listener for ingest job application events.
|
|
*/
|
|
static private class IngestJobEventListener implements PropertyChangeListener {
|
|
|
|
@NbBundle.Messages({
|
|
"ImageGalleryController.dataSourceAnalyzed.confDlg.msg= A new data source was added and finished ingest.\n"
|
|
+ "The image / video database may be out of date. "
|
|
+ "Do you want to update the database with ingest results?\n",
|
|
"ImageGalleryController.dataSourceAnalyzed.confDlg.title=Image Gallery"
|
|
})
|
|
@Override
|
|
public void propertyChange(PropertyChangeEvent event) {
|
|
/*
|
|
* Only handling data source analysis events.
|
|
*/
|
|
// RJCTODO: DO we need to handle any events at all on an auot ingest node?
|
|
if (!(event instanceof DataSourceAnalysisEvent)) {
|
|
return;
|
|
}
|
|
|
|
ImageGalleryController controller;
|
|
try {
|
|
controller = getController();
|
|
// RJCTODO: I think it would be best to move handling of these
|
|
// case events into the controller class and have the controller
|
|
// instance register/unregister as a listener when it is
|
|
// contructed and shuts down. This will improve the encapsulation
|
|
// of ImageGalleryController and allow it to check its own open/closed
|
|
// state before handling an event.
|
|
} catch (TskCoreException ex) {
|
|
logger.log(Level.SEVERE, String.format("Failed to handle %s event", event.getPropertyName()), ex); //NON-NLS
|
|
return;
|
|
}
|
|
|
|
DataSourceAnalysisEvent dataSourceEvent = (DataSourceAnalysisEvent) event;
|
|
Content dataSource = dataSourceEvent.getDataSource();
|
|
long dataSourceObjId = dataSource.getId();
|
|
String eventType = dataSourceEvent.getPropertyName();
|
|
try {
|
|
switch (IngestManager.IngestJobEvent.valueOf(eventType)) {
|
|
case DATA_SOURCE_ANALYSIS_STARTED:
|
|
if (((AutopsyEvent) event).getSourceType() == AutopsyEvent.SourceType.LOCAL) {
|
|
if (controller.isListeningEnabled()) {
|
|
DrawableDB drawableDb = controller.getDatabase();
|
|
// Don't update status if it is is already marked as COMPLETE
|
|
if (drawableDb.getDataSourceDbBuildStatus(dataSourceObjId) != DrawableDB.DrawableDbBuildStatusEnum.COMPLETE) {
|
|
drawableDb.insertOrUpdateDataSource(dataSource.getId(), DrawableDB.DrawableDbBuildStatusEnum.IN_PROGRESS);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case DATA_SOURCE_ANALYSIS_COMPLETED:
|
|
if (((AutopsyEvent) event).getSourceType() == AutopsyEvent.SourceType.LOCAL) {
|
|
/*
|
|
* This node just completed analysis of a data
|
|
* source. Set the state of the local drawables
|
|
* database.
|
|
*/
|
|
if (controller.isListeningEnabled()) {
|
|
DrawableDB drawableDb = controller.getDatabase();
|
|
if (drawableDb.getDataSourceDbBuildStatus(dataSourceObjId) == DrawableDB.DrawableDbBuildStatusEnum.IN_PROGRESS) {
|
|
|
|
// If at least one file in CaseDB has mime type, then set to COMPLETE
|
|
// Otherwise, back to UNKNOWN since we assume file type module was not run
|
|
DrawableDB.DrawableDbBuildStatusEnum datasourceDrawableDBStatus
|
|
= controller.hasFilesWithMimeType(dataSourceObjId)
|
|
? DrawableDB.DrawableDbBuildStatusEnum.COMPLETE
|
|
: DrawableDB.DrawableDbBuildStatusEnum.UNKNOWN;
|
|
|
|
controller.getDatabase().insertOrUpdateDataSource(dataSource.getId(), datasourceDrawableDBStatus);
|
|
}
|
|
}
|
|
} else if (((AutopsyEvent) event).getSourceType() == AutopsyEvent.SourceType.REMOTE) {
|
|
/*
|
|
* A remote node just completed analysis of a data
|
|
* source. The local drawables database is therefore
|
|
* stale. If the image gallery top component is
|
|
* open, give the user an opportunity to update the
|
|
* drawables database now.
|
|
*/
|
|
controller.setCaseStale(true);
|
|
if (controller.isListeningEnabled()) {
|
|
SwingUtilities.invokeLater(() -> {
|
|
if (ImageGalleryTopComponent.isImageGalleryOpen()) {
|
|
int showAnswer = JOptionPane.showConfirmDialog(ImageGalleryTopComponent.getTopComponent(),
|
|
Bundle.ImageGalleryController_dataSourceAnalyzed_confDlg_msg(),
|
|
Bundle.ImageGalleryController_dataSourceAnalyzed_confDlg_title(),
|
|
JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
|
|
switch (showAnswer) {
|
|
case JOptionPane.YES_OPTION:
|
|
controller.rebuildDB();
|
|
break;
|
|
case JOptionPane.NO_OPTION:
|
|
case JOptionPane.CANCEL_OPTION:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
} catch (TskCoreException ex) {
|
|
logger.log(Level.SEVERE, String.format("Failed to handle %s event for %s (objId=%d)", dataSourceEvent.getPropertyName(), dataSource.getName(), dataSourceObjId), ex);
|
|
}
|
|
}
|
|
}
|
|
}
|