mirror of
https://github.com/overcuriousity/autopsy-flatpak.git
synced 2025-07-06 21:00:22 +00:00
365 lines
16 KiB
Java
365 lines
16 KiB
Java
/*
|
|
* Autopsy Forensic Browser
|
|
*
|
|
* Copyright 2013-18 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.logging.Level;
|
|
import javafx.application.Platform;
|
|
import javax.swing.JOptionPane;
|
|
import javax.swing.SwingUtilities;
|
|
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
|
import org.openide.util.Exceptions;
|
|
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.coreutils.MessageNotifyUtil;
|
|
import org.sleuthkit.autopsy.events.AutopsyEvent;
|
|
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB;
|
|
import org.sleuthkit.autopsy.ingest.IngestManager;
|
|
import org.sleuthkit.autopsy.ingest.IngestManager.IngestJobEvent;
|
|
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.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;
|
|
|
|
/** static definitions, utilities, and listeners for the ImageGallery module */
|
|
@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 Object controllerLock = new Object();
|
|
private static ImageGalleryController controller;
|
|
|
|
public static ImageGalleryController getController() throws TskCoreException, NoCurrentCaseException {
|
|
synchronized (controllerLock) {
|
|
if (controller == null) {
|
|
controller = new ImageGalleryController(Case.getCurrentCaseThrows());
|
|
}
|
|
return controller;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*
|
|
* This method is invoked by virtue of the OnStart annotation on the OnStart
|
|
* class class
|
|
*/
|
|
static void onStart() {
|
|
Platform.setImplicitExit(false);
|
|
logger.info("Setting up ImageGallery listeners"); //NON-NLS
|
|
|
|
IngestManager.getInstance().addIngestJobEventListener(new IngestJobEventListener());
|
|
IngestManager.getInstance().addIngestModuleEventListener(new IngestModuleEventListener());
|
|
Case.addPropertyChangeListener(new CaseEventListener());
|
|
}
|
|
|
|
static String getModuleName() {
|
|
return MODULE_NAME;
|
|
}
|
|
|
|
/**
|
|
* get the Path to the Case's ImageGallery ModuleOutput subfolder; ie
|
|
* ".../[CaseName]/ModuleOutput/Image Gallery/"
|
|
*
|
|
* @param theCase the case to get the ImageGallery ModuleOutput subfolder
|
|
* for
|
|
*
|
|
* @return the Path to the ModuleOuput subfolder for Image Gallery
|
|
*/
|
|
public static Path getModuleOutputDir(Case theCase) {
|
|
return Paths.get(theCase.getModuleDirectory(), getModuleName());
|
|
}
|
|
|
|
/** provides static utilities, can not be instantiated */
|
|
private ImageGalleryModule() {
|
|
}
|
|
|
|
/** is listening enabled for the given case
|
|
*
|
|
* @param c
|
|
*
|
|
* @return true if listening is enabled for the given case, false otherwise
|
|
*/
|
|
static boolean isEnabledforCase(Case c) {
|
|
if (c != null) {
|
|
String enabledforCaseProp = new PerCaseProperties(c).getConfigSetting(ImageGalleryModule.MODULE_NAME, PerCaseProperties.ENABLED);
|
|
return isNotBlank(enabledforCaseProp) ? Boolean.valueOf(enabledforCaseProp) : ImageGalleryPreferences.isEnabledByDefault();
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Is the given file 'supported' and not 'known'(nsrl hash hit). If so we
|
|
* should include it in {@link DrawableDB} and UI
|
|
*
|
|
* @param abstractFile
|
|
*
|
|
* @return true if the given {@link AbstractFile} is "drawable" and not
|
|
* 'known', else false
|
|
*
|
|
* @throws
|
|
* org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector.FileTypeDetectorInitException
|
|
*/
|
|
public static boolean isDrawableAndNotKnown(AbstractFile abstractFile) throws FileTypeDetector.FileTypeDetectorInitException {
|
|
return (abstractFile.getKnown() != TskData.FileKnown.KNOWN) && FileTypeUtils.isDrawable(abstractFile);
|
|
}
|
|
|
|
/**
|
|
* Listener for IngestModuleEvents
|
|
*/
|
|
static private class IngestModuleEventListener implements PropertyChangeListener {
|
|
|
|
@Override
|
|
public void propertyChange(PropertyChangeEvent evt) {
|
|
if (RuntimeProperties.runningWithGUI() == false) {
|
|
/*
|
|
* Running in "headless" mode, no need to process any events.
|
|
* This cannot be done earlier because the switch to core
|
|
* components inactive may not have been made at start up.
|
|
*/
|
|
IngestManager.getInstance().removeIngestModuleEventListener(this);
|
|
return;
|
|
}
|
|
|
|
/* only process individual files in realtime on the node that is
|
|
* running the ingest. on a remote node, image files are processed
|
|
* enblock when ingest is complete */
|
|
if (((AutopsyEvent) evt).getSourceType() != AutopsyEvent.SourceType.LOCAL) {
|
|
return;
|
|
}
|
|
|
|
// Bail out if the case is closed
|
|
try {
|
|
if (controller == null || Case.getCurrentCaseThrows() == null) {
|
|
return;
|
|
}
|
|
} catch (NoCurrentCaseException ex) {
|
|
return;
|
|
}
|
|
|
|
if (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName()) == FILE_DONE) {
|
|
|
|
// getOldValue has fileID getNewValue has Abstractfile
|
|
AbstractFile file = (AbstractFile) evt.getNewValue();
|
|
if (false == file.isFile()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
ImageGalleryController con = getController();
|
|
if (con.isListeningEnabled()) {
|
|
try {
|
|
// Update the entry if it is a picture and not in NSRL
|
|
if (isDrawableAndNotKnown(file)) {
|
|
con.queueDBTask(new ImageGalleryController.UpdateFileTask(file, controller.getDatabase()));
|
|
}
|
|
// Remove it from the DB if it is no longer relevant, but had the correct extension
|
|
else if (FileTypeUtils.getAllSupportedExtensions().contains(file.getNameExtension())) {
|
|
/* Doing this check results in fewer tasks queued
|
|
* up, and faster completion of db update. This file
|
|
* would have gotten scooped up in initial grab, but
|
|
* actually we don't need it */
|
|
con.queueDBTask(new ImageGalleryController.RemoveFileTask(file, controller.getDatabase()));
|
|
}
|
|
} catch (FileTypeDetector.FileTypeDetectorInitException ex) {
|
|
logger.log(Level.SEVERE, "Unable to determine if file is drawable and not known. Not making any changes to DB", ex); //NON-NLS
|
|
MessageNotifyUtil.Notify.error("Image Gallery Error",
|
|
"Unable to determine if file is drawable and not known. Not making any changes to DB. See the logs for details.");
|
|
}
|
|
}
|
|
} catch (NoCurrentCaseException ex) {
|
|
logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS
|
|
} catch (TskCoreException ex) {
|
|
logger.log(Level.SEVERE, "Error getting ImageGalleryController.", ex); //NON-NLS
|
|
}
|
|
}
|
|
else if (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName()) == DATA_ADDED) {
|
|
ModuleDataEvent mde = (ModuleDataEvent)evt.getOldValue();
|
|
|
|
if (mde.getBlackboardArtifactType().getTypeID() == ARTIFACT_TYPE.TSK_METADATA_EXIF.getTypeID()) {
|
|
DrawableDB drawableDB = controller.getDatabase();
|
|
if (mde.getArtifacts() != null) {
|
|
for (BlackboardArtifact art : mde.getArtifacts()) {
|
|
drawableDB.addExifCache(art.getObjectID());
|
|
}
|
|
}
|
|
}
|
|
else if (mde.getBlackboardArtifactType().getTypeID() == ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID()) {
|
|
DrawableDB drawableDB = controller.getDatabase();
|
|
if (mde.getArtifacts() != null) {
|
|
for (BlackboardArtifact art : mde.getArtifacts()) {
|
|
drawableDB.addHashSetCache(art.getObjectID());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listener for case events.
|
|
*/
|
|
static private class CaseEventListener implements PropertyChangeListener {
|
|
|
|
@Override
|
|
public void propertyChange(PropertyChangeEvent evt) {
|
|
if (RuntimeProperties.runningWithGUI() == false) {
|
|
/*
|
|
* Running in "headless" mode, no need to process any events.
|
|
* This cannot be done earlier because the switch to core
|
|
* components inactive may not have been made at start up.
|
|
*/
|
|
Case.removePropertyChangeListener(this);
|
|
return;
|
|
}
|
|
ImageGalleryController con;
|
|
try {
|
|
con = getController();
|
|
} catch (NoCurrentCaseException ex) {
|
|
logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS
|
|
return;
|
|
} catch (TskCoreException ex) {
|
|
logger.log(Level.SEVERE, "Error getting ImageGalleryController.", ex); //NON-NLS
|
|
return;
|
|
}
|
|
switch (Case.Events.valueOf(evt.getPropertyName())) {
|
|
case CURRENT_CASE:
|
|
synchronized (controllerLock) {
|
|
// case has changes: close window, reset everything
|
|
SwingUtilities.invokeLater(ImageGalleryTopComponent::closeTopComponent);
|
|
if (controller != null) {
|
|
controller.reset();
|
|
}
|
|
controller = null;
|
|
|
|
Case newCase = (Case) evt.getNewValue();
|
|
if (newCase != null) {
|
|
// a new case has been opened: connect db, groupmanager, start worker thread
|
|
try {
|
|
controller = new ImageGalleryController(newCase);
|
|
} catch (TskCoreException ex) {
|
|
logger.log(Level.SEVERE, "Error changing case in ImageGallery.", ex);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case DATA_SOURCE_ADDED:
|
|
//For a data source added on the local node, prepopulate all file data to drawable database
|
|
if (((AutopsyEvent) evt).getSourceType() == AutopsyEvent.SourceType.LOCAL) {
|
|
Content newDataSource = (Content) evt.getNewValue();
|
|
if (con.isListeningEnabled()) {
|
|
con.queueDBTask(new ImageGalleryController.PrePopulateDataSourceFiles(newDataSource.getId(), controller));
|
|
}
|
|
}
|
|
break;
|
|
case CONTENT_TAG_ADDED:
|
|
final ContentTagAddedEvent tagAddedEvent = (ContentTagAddedEvent) evt;
|
|
|
|
long objId = tagAddedEvent.getAddedTag().getContent().getId();
|
|
|
|
// update the cache
|
|
DrawableDB drawableDB = controller.getDatabase();
|
|
drawableDB.addTagCache(objId);
|
|
|
|
if (con.getDatabase().isInDB(objId)) {
|
|
con.getTagsManager().fireTagAddedEvent(tagAddedEvent);
|
|
}
|
|
break;
|
|
case CONTENT_TAG_DELETED:
|
|
final ContentTagDeletedEvent tagDeletedEvent = (ContentTagDeletedEvent) evt;
|
|
if (con.getDatabase().isInDB(tagDeletedEvent.getDeletedTagInfo().getContentID())) {
|
|
con.getTagsManager().fireTagDeletedEvent(tagDeletedEvent);
|
|
}
|
|
break;
|
|
default:
|
|
//we don't need to do anything for other events.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listener for Ingest Job 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 evt) {
|
|
IngestJobEvent eventType = IngestJobEvent.valueOf(evt.getPropertyName());
|
|
if (eventType != IngestJobEvent.DATA_SOURCE_ANALYSIS_COMPLETED
|
|
|| ((AutopsyEvent) evt).getSourceType() != AutopsyEvent.SourceType.REMOTE) {
|
|
return;
|
|
}
|
|
// A remote node added a new data source and just finished ingest on it.
|
|
//drawable db is stale, and if ImageGallery is open, ask user what to do
|
|
|
|
try {
|
|
ImageGalleryController con = getController();
|
|
con.setStale(true);
|
|
if (con.isListeningEnabled() && ImageGalleryTopComponent.isImageGalleryOpen()) {
|
|
SwingUtilities.invokeLater(() -> {
|
|
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:
|
|
con.rebuildDB();
|
|
break;
|
|
case JOptionPane.NO_OPTION:
|
|
case JOptionPane.CANCEL_OPTION:
|
|
default:
|
|
break; //do nothing
|
|
}
|
|
});
|
|
}
|
|
} catch (NoCurrentCaseException ex) {
|
|
logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS
|
|
} catch (TskCoreException ex) {
|
|
logger.log(Level.SEVERE, "Error getting ImageGalleryController.", ex); //NON-NLS
|
|
}
|
|
}
|
|
}
|
|
}
|