diff --git a/BUILDING.txt b/BUILDING.txt index 0d3e3ed9bd..a77d6c8add 100644 --- a/BUILDING.txt +++ b/BUILDING.txt @@ -37,16 +37,16 @@ to the root 64-bit JRE directory. 2) Get Sleuth Kit Setup 2a) Download and build a Release version of Sleuth Kit (TSK) 4.0. See win32\BUILDING.txt in the TSK package for more information. You need to - build the tsk_jni project. Select the Release_PostgreSQL Win32 or x64 target, + build the tsk_jni project. Select the Release Win32 or x64 target, depending upon your target build. You can use a released version or download the latest from github: - git://github.com/sleuthkit/sleuthkit.git -2b) Build the TSK JAR file by typing 'ant dist-PostgreSQL' in +2b) Build the TSK JAR file by typing 'ant dist' in bindings/java in the TSK source code folder from a command line. Note it is case sensitive. You can also add the code to a NetBeans project and build - it from there, selecting the dist-PostgreSQL target. + it from there, selecting the dist target. 2c) Set TSK_HOME environment variable to the root directory of TSK @@ -54,6 +54,9 @@ to the root 64-bit JRE directory. from the TSK root directory to install the libraries and such in the needed places (i.e. '/usr/local'). +2e) Build the TSK CaseUco jar file by running 'ant' in + the case-uco/java folder of the TSK source folder. You can also add the + code to a NetBeans project and build using the regular 'build' action. 3) For Windows builds, GStreamer must be setup. GStreamer is used to view video files. You can either download it and install it, or you can copy it from the @@ -100,7 +103,7 @@ the build process. - The Sleuth Kit Java datamodel JAR file has native JNI libraries that are copied into it. These JNI libraries have dependencies on -libewf, zlib, libpq, libintl-8, libeay32, and ssleay32 DLL files. On non-Windows +libewf, zlib, libintl-8, libeay32, and ssleay32 DLL files. On non-Windows platforms, the JNI library also has a dependency on libtsk (on Windows, it is compiled into libtsk_jni). diff --git a/Core/build.xml b/Core/build.xml index a1290ef2e8..52f12a4b0b 100644 --- a/Core/build.xml +++ b/Core/build.xml @@ -54,6 +54,16 @@ + + + + + + + + + + @@ -105,7 +115,9 @@ + tofile="${ext.dir}/SparseBitSet-1.1.jar"/> + diff --git a/Core/manifest.mf b/Core/manifest.mf index 00681f3e7f..0f9b4579f6 100644 --- a/Core/manifest.mf +++ b/Core/manifest.mf @@ -2,7 +2,7 @@ Manifest-Version: 1.0 OpenIDE-Module: org.sleuthkit.autopsy.core/10 OpenIDE-Module-Localizing-Bundle: org/sleuthkit/autopsy/core/Bundle.properties OpenIDE-Module-Layer: org/sleuthkit/autopsy/core/layer.xml -OpenIDE-Module-Implementation-Version: 31 +OpenIDE-Module-Implementation-Version: 32 OpenIDE-Module-Requires: org.openide.windows.WindowManager AutoUpdate-Show-In-Client: true AutoUpdate-Essential-Module: true diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties index 3bf03f2103..8dbd7f8d92 100644 --- a/Core/nbproject/project.properties +++ b/Core/nbproject/project.properties @@ -83,6 +83,7 @@ file.reference.sevenzipjbinding.jar=release/modules/ext/sevenzipjbinding.jar file.reference.sis-metadata-0.8.jar=release\\modules\\ext\\sis-metadata-0.8.jar file.reference.sis-netcdf-0.8.jar=release\\modules\\ext\\sis-netcdf-0.8.jar file.reference.sis-utility-0.8.jar=release\\modules\\ext\\sis-utility-0.8.jar +file.reference.sleuthkit-caseuco-4.10.0.jar=release/modules/ext/sleuthkit-caseuco-4.10.0.jar file.reference.slf4j-api-1.7.25.jar=release\\modules\\ext\\slf4j-api-1.7.25.jar file.reference.sqlite-jdbc-3.25.2.jar=release/modules/ext/sqlite-jdbc-3.25.2.jar file.reference.StixLib.jar=release/modules/ext/StixLib.jar @@ -90,7 +91,7 @@ file.reference.javax.ws.rs-api-2.0.1.jar=release/modules/ext/javax.ws.rs-api-2.0 file.reference.cxf-core-3.0.16.jar=release/modules/ext/cxf-core-3.0.16.jar file.reference.cxf-rt-frontend-jaxrs-3.0.16.jar=release/modules/ext/cxf-rt-frontend-jaxrs-3.0.16.jar file.reference.cxf-rt-transports-http-3.0.16.jar=release/modules/ext/cxf-rt-transports-http-3.0.16.jar -file.reference.sleuthkit-4.9.0.jar=release/modules/ext/sleuthkit-4.9.0.jar +file.reference.sleuthkit-4.10.0.jar=release/modules/ext/sleuthkit-4.10.0.jar file.reference.curator-client-2.8.0.jar=release/modules/ext/curator-client-2.8.0.jar file.reference.curator-framework-2.8.0.jar=release/modules/ext/curator-framework-2.8.0.jar file.reference.curator-recipes-2.8.0.jar=release/modules/ext/curator-recipes-2.8.0.jar @@ -138,5 +139,5 @@ nbm.homepage=http://www.sleuthkit.org/ nbm.module.author=Brian Carrier nbm.needs.restart=true source.reference.curator-recipes-2.8.0.jar=release/modules/ext/curator-recipes-2.8.0-sources.jar -spec.version.base=10.19 +spec.version.base=10.20 diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml index e06cc6ca29..61e6a86b04 100644 --- a/Core/nbproject/project.xml +++ b/Core/nbproject/project.xml @@ -472,8 +472,8 @@ release/modules/ext/commons-pool2-2.4.2.jar - ext/sleuthkit-4.9.0.jar - release/modules/ext/sleuthkit-4.9.0.jar + ext/sleuthkit-4.10.0.jar + release/modules/ext/sleuthkit-4.10.0.jar ext/jxmapviewer2-2.4.jar @@ -779,6 +779,10 @@ ext/curator-client-2.8.0.jar release/modules/ext/curator-client-2.8.0.jar + + ext/sleuthkit-caseuco-4.10.0.jar + release/modules/ext/sleuthkit-caseuco-4.10.0.jar + ext/fontbox-2.0.13.jar release\modules\ext\fontbox-2.0.13.jar diff --git a/Core/src/org/sleuthkit/autopsy/actions/AddTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/AddTagAction.java index 53cf076c2e..9c059205f5 100644 --- a/Core/src/org/sleuthkit/autopsy/actions/AddTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/AddTagAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2019 Basis Technology Corp. + * Copyright 2013-2020 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -139,7 +139,7 @@ abstract class AddTagAction extends AbstractAction implements Presenter.Popup { if (!tagNamesMap.isEmpty()) { for (Map.Entry entry : tagNamesMap.entrySet()) { TagName tagName = entry.getValue(); - TagSet tagSet = tagName.getTagSet(); + TagSet tagSet = tagsManager.getTagSet(tagName); // Show custom tags before predefined tags in the menu if (tagSet != null) { diff --git a/Core/src/org/sleuthkit/autopsy/actions/DeleteContentTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/DeleteContentTagAction.java index b7ff3a73a0..b6c882cd5e 100644 --- a/Core/src/org/sleuthkit/autopsy/actions/DeleteContentTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/DeleteContentTagAction.java @@ -29,6 +29,9 @@ import org.openide.util.Utilities; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager; +import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager.ContentViewerTag; +import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTagRegion; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.TskCoreException; @@ -72,6 +75,12 @@ public class DeleteContentTagAction extends AbstractAction { new Thread(() -> { for (ContentTag tag : selectedTags) { try { + // Check if there is an image tag before deleting the content tag. + ContentViewerTag imageTag = ContentViewerTagManager.getTag(tag, ImageTagRegion.class); + if(imageTag != null) { + ContentViewerTagManager.deleteTag(imageTag); + } + Case.getCurrentCaseThrows().getServices().getTagsManager().deleteContentTag(tag); } catch (TskCoreException | NoCurrentCaseException ex) { Logger.getLogger(DeleteContentTagAction.class.getName()).log(Level.SEVERE, "Error deleting tag", ex); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/actions/DeleteFileContentTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/DeleteFileContentTagAction.java index cb719e2f0a..4141e3d29d 100644 --- a/Core/src/org/sleuthkit/autopsy/actions/DeleteFileContentTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/DeleteFileContentTagAction.java @@ -39,6 +39,9 @@ import org.openide.util.actions.Presenter; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.services.TagsManager; +import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager; +import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager.ContentViewerTag; +import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTagRegion; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.tags.TagUtils; import org.sleuthkit.datamodel.AbstractFile; @@ -123,6 +126,13 @@ public class DeleteFileContentTagAction extends AbstractAction implements Presen try { logger.log(Level.INFO, "Removing tag {0} from {1}", new Object[]{tagName.getDisplayName(), contentTag.getContent().getName()}); //NON-NLS + + // Check if there is an image tag before deleting the content tag. + ContentViewerTag imageTag = ContentViewerTagManager.getTag(contentTag, ImageTagRegion.class); + if(imageTag != null) { + ContentViewerTagManager.deleteTag(imageTag); + } + tagsManager.deleteContentTag(contentTag); } catch (TskCoreException tskCoreException) { logger.log(Level.SEVERE, "Error untagging file", tskCoreException); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/actions/GetTagNameAndCommentDialog.java b/Core/src/org/sleuthkit/autopsy/actions/GetTagNameAndCommentDialog.java index ba586c7b22..a0c3b1c5c0 100644 --- a/Core/src/org/sleuthkit/autopsy/actions/GetTagNameAndCommentDialog.java +++ b/Core/src/org/sleuthkit/autopsy/actions/GetTagNameAndCommentDialog.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2018 Basis Technology Corp. + * Copyright 2013-2020 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +23,6 @@ import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.util.ArrayList; -import java.util.HashMap; import java.util.logging.Level; import java.util.List; import java.util.Map; @@ -36,7 +35,6 @@ import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JList; import javax.swing.KeyStroke; -import org.openide.util.Exceptions; import org.openide.util.NbBundle; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; @@ -150,7 +148,7 @@ public class GetTagNameAndCommentDialog extends JDialog { } ); - try { + try { TagsManager tagsManager = Case.getCurrentCaseThrows().getServices().getTagsManager(); List standardTagNames = TagsManager.getStandardTagNames(); Map tagNamesMap = new TreeMap<>(tagsManager.getDisplayNamesToTagNamesMap()); @@ -161,7 +159,7 @@ public class GetTagNameAndCommentDialog extends JDialog { tagNamesMap.entrySet().stream().map((entry) -> entry.getValue()).forEachOrdered((tagName) -> { TagSet tagSet = null; try { - tagSet = tagName.getTagSet(); + tagSet = tagsManager.getTagSet(tagName); } catch (TskCoreException ex) { Logger.getLogger(GetTagNameAndCommentDialog.class .getName()).log(Level.SEVERE, "Failed to get tag set", ex); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/actions/ReplaceContentTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/ReplaceContentTagAction.java index 51b898eb84..1c69bc133c 100644 --- a/Core/src/org/sleuthkit/autopsy/actions/ReplaceContentTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/ReplaceContentTagAction.java @@ -29,6 +29,9 @@ import org.openide.util.Utilities; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.services.TagsManager; +import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager; +import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager.ContentViewerTag; +import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTagRegion; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.TagName; @@ -83,9 +86,19 @@ public final class ReplaceContentTagAction extends ReplaceTagAction try { logger.log(Level.INFO, "Replacing tag {0} with tag {1} for artifact {2}", new Object[]{oldTag.getName().getDisplayName(), newTagName.getDisplayName(), oldTag.getContent().getName()}); //NON-NLS + // Check if there is an image tag before deleting the content tag. + ContentViewerTag imageTag = ContentViewerTagManager.getTag(oldTag, ImageTagRegion.class); + if(imageTag != null) { + ContentViewerTagManager.deleteTag(imageTag); + } + tagsManager.deleteContentTag(oldTag); - tagsManager.addContentTag(oldTag.getContent(), newTagName, newComment); - + ContentTag newTag = tagsManager.addContentTag(oldTag.getContent(), newTagName, newComment); + + // Resave the image tag if present. + if(imageTag != null) { + ContentViewerTagManager.saveTag(newTag, imageTag.getDetails()); + } } catch (TskCoreException tskCoreException) { logger.log(Level.SEVERE, "Error replacing artifact tag", tskCoreException); //NON-NLS Platform.runLater(() diff --git a/Core/src/org/sleuthkit/autopsy/actions/ReplaceTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/ReplaceTagAction.java index 91929db577..34ed836e3e 100644 --- a/Core/src/org/sleuthkit/autopsy/actions/ReplaceTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/ReplaceTagAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2018 Basis Technology Corp. + * Copyright 2018-2020 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -131,7 +131,7 @@ abstract class ReplaceTagAction extends AbstractAction implements if (!tagNamesMap.isEmpty()) { for (Map.Entry entry : tagNamesMap.entrySet()) { TagName tagName = entry.getValue(); - TagSet tagSet = tagName.getTagSet(); + TagSet tagSet = tagsManager.getTagSet(tagName); // Show custom tags before predefined tags in the menu if (tagSet != null) { diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTask.java b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTask.java index a1443bde31..53320d9313 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTask.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTask.java @@ -30,6 +30,7 @@ import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgress import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.imagewriter.ImageWriterService; import org.sleuthkit.autopsy.imagewriter.ImageWriterSettings; +import org.sleuthkit.datamodel.AddDataSourceCallbacks; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.Image; import org.sleuthkit.datamodel.SleuthkitJNI; @@ -42,17 +43,10 @@ import org.sleuthkit.datamodel.TskDataException; class AddImageTask implements Runnable { private final Logger logger = Logger.getLogger(AddImageTask.class.getName()); - private final String deviceId; - private final String imagePath; - private final int sectorSize; - private final String timeZone; - private final ImageWriterSettings imageWriterSettings; - private final boolean ignoreFatOrphanFiles; - private final String md5; - private final String sha1; - private final String sha256; + private final ImageDetails imageDetails; private final DataSourceProcessorProgressMonitor progressMonitor; - private final DataSourceProcessorCallback callback; + private final AddDataSourceCallbacks addDataSourceCallbacks; + private final AddImageTaskCallback addImageTaskCallback; private boolean criticalErrorOccurred; /* @@ -73,40 +67,18 @@ class AddImageTask implements Runnable { /** * Constructs a runnable task that adds an image to the case database. - * - * @param deviceId An ASCII-printable identifier for the device - * associated with the data source that is - * intended to be unique across multiple cases - * (e.g., a UUID). - * @param imagePath Path to the image file. - * @param sectorSize The sector size (use '0' for autodetect). - * @param timeZone The time zone to use when processing dates - * and times for the image, obtained from - * java.util.TimeZone.getID. - * @param ignoreFatOrphanFiles Whether to parse orphans if the image has a - * FAT filesystem. - * @param md5 The MD5 hash of the image, may be null. - * @param sha1 The SHA-1 hash of the image, may be null. - * @param sha256 The SHA-256 hash of the image, may be null. - * @param imageWriterPath Path that a copy of the image should be - * written to. Use empty string to disable image - * writing + * + * @param imageDetails Holds all data about the image. * @param progressMonitor Progress monitor to report progress during * processing. - * @param callback Callback to call when processing is done. + * @param addDataSourceCallbacks Callback for sending data to the ingest pipeline if an ingest stream is being used. + * @param addImageTaskCallback Callback for dealing with add image task completion. */ - AddImageTask(String deviceId, String imagePath, int sectorSize, String timeZone, boolean ignoreFatOrphanFiles, String md5, String sha1, String sha256, ImageWriterSettings imageWriterSettings, - DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { - this.deviceId = deviceId; - this.imagePath = imagePath; - this.sectorSize = sectorSize; - this.timeZone = timeZone; - this.ignoreFatOrphanFiles = ignoreFatOrphanFiles; - this.md5 = md5; - this.sha1 = sha1; - this.sha256 = sha256; - this.imageWriterSettings = imageWriterSettings; - this.callback = callback; + AddImageTask(ImageDetails imageDetails, DataSourceProcessorProgressMonitor progressMonitor, AddDataSourceCallbacks addDataSourceCallbacks, + AddImageTaskCallback addImageTaskCallback) { + this.imageDetails = imageDetails; + this.addDataSourceCallbacks = addDataSourceCallbacks; + this.addImageTaskCallback = addImageTaskCallback; this.progressMonitor = progressMonitor; tskAddImageProcessLock = new Object(); } @@ -120,21 +92,21 @@ class AddImageTask implements Runnable { try { currentCase = Case.getCurrentCaseThrows(); } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, String.format("Failed to add image data source at %s, no current case", imagePath), ex); + logger.log(Level.SEVERE, String.format("Failed to start AddImageTask for %s, no current case", imageDetails.getImagePath()), ex); return; } progressMonitor.setIndeterminate(true); progressMonitor.setProgress(0); String imageWriterPath = ""; - if (imageWriterSettings != null) { - imageWriterPath = imageWriterSettings.getPath(); + if (imageDetails.imageWriterSettings != null) { + imageWriterPath = imageDetails.imageWriterSettings.getPath(); } List errorMessages = new ArrayList<>(); List newDataSources = new ArrayList<>(); try { synchronized (tskAddImageProcessLock) { if (!tskAddImageProcessStopped) { - tskAddImageProcess = currentCase.getSleuthkitCase().makeAddImageProcess(timeZone, true, ignoreFatOrphanFiles, imageWriterPath); + tskAddImageProcess = currentCase.getSleuthkitCase().makeAddImageProcess(imageDetails.timeZone, true, imageDetails.ignoreFatOrphanFiles, imageWriterPath); } else { return; } @@ -143,7 +115,7 @@ class AddImageTask implements Runnable { progressUpdateThread.start(); runAddImageProcess(errorMessages); progressUpdateThread.interrupt(); - commitOrRevertAddImageProcess(currentCase, errorMessages, newDataSources); + finishAddImageProcess(errorMessages, newDataSources); progressMonitor.setProgress(100); } finally { DataSourceProcessorCallback.DataSourceProcessorResult result; @@ -154,7 +126,7 @@ class AddImageTask implements Runnable { } else { result = DataSourceProcessorResult.NO_ERRORS; } - callback.done(result, errorMessages, newDataSources); + addImageTaskCallback.onCompleted(result, errorMessages, newDataSources); } } @@ -177,7 +149,7 @@ class AddImageTask implements Runnable { tskAddImageProcess.stop(); } catch (TskCoreException ex) { - logger.log(Level.SEVERE, String.format("Error cancelling adding image %s to the case database", imagePath), ex); //NON-NLS + logger.log(Level.SEVERE, String.format("Error cancelling adding image %s to the case database", imageDetails.getImagePath()), ex); //NON-NLS } } } @@ -191,23 +163,22 @@ class AddImageTask implements Runnable { */ private void runAddImageProcess(List errorMessages) { try { - tskAddImageProcess.run(deviceId, new String[]{imagePath}, sectorSize); + tskAddImageProcess.run(imageDetails.deviceId, imageDetails.image, imageDetails.sectorSize, this.addDataSourceCallbacks); } catch (TskCoreException ex) { - logger.log(Level.SEVERE, String.format("Critical error occurred adding image %s", imagePath), ex); //NON-NLS + logger.log(Level.SEVERE, String.format("Critical error occurred adding image %s", imageDetails.getImagePath()), ex); //NON-NLS criticalErrorOccurred = true; errorMessages.add(ex.getMessage()); } catch (TskDataException ex) { - logger.log(Level.WARNING, String.format("Non-critical error occurred adding image %s", imagePath), ex); //NON-NLS + logger.log(Level.WARNING, String.format("Non-critical error occurred adding image %s", imageDetails.getImagePath()), ex); //NON-NLS errorMessages.add(ex.getMessage()); } } /** - * Commits or reverts the results of the TSK add image process. If the - * process was stopped before it completed or there was a critical error the - * results are reverted, otherwise they are committed. + * Handle the results of the TSK add image process. + * The image will be in the database even if a critical error occurred or + * the user canceled. * - * @param currentCase The current case. * @param errorMessages Error messages, if any, are added to this list for * eventual return via the callback. * @param newDataSources If the new image is successfully committed, it is @@ -216,84 +187,66 @@ class AddImageTask implements Runnable { * * @return */ - private void commitOrRevertAddImageProcess(Case currentCase, List errorMessages, List newDataSources) { + private void finishAddImageProcess(List errorMessages, List newDataSources) { synchronized (tskAddImageProcessLock) { - if (tskAddImageProcessStopped || criticalErrorOccurred) { + Image newImage = imageDetails.image; + String verificationError = newImage.verifyImageSize(); + if (!verificationError.isEmpty()) { + errorMessages.add(verificationError); + } + if (imageDetails.imageWriterSettings != null) { + ImageWriterService.createImageWriter(newImage.getId(), imageDetails.imageWriterSettings); + } + newDataSources.add(newImage); + + // If the add image process was cancelled don't do any further processing here + if (tskAddImageProcessStopped) { + return; + } + + if (!StringUtils.isBlank(imageDetails.md5)) { try { - tskAddImageProcess.revert(); + newImage.setMD5(imageDetails.md5); } catch (TskCoreException ex) { - logger.log(Level.SEVERE, String.format("Error reverting after adding image %s to the case database", imagePath), ex); //NON-NLS + logger.log(Level.SEVERE, String.format("Failed to add MD5 hash for image data source %s (objId=%d)", newImage.getName(), newImage.getId()), ex); errorMessages.add(ex.getMessage()); criticalErrorOccurred = true; + } catch (TskDataException ignored) { + /* + * The only reasonable way for this to happen at + * present is through C/C++ processing of an EWF + * image, which is not an error. + */ } - } else { + } + if (!StringUtils.isBlank(imageDetails.sha1)) { try { - long imageId = tskAddImageProcess.commit(); - if (imageId != 0) { - Image newImage = currentCase.getSleuthkitCase().getImageById(imageId); - String verificationError = newImage.verifyImageSize(); - if (!verificationError.isEmpty()) { - errorMessages.add(verificationError); - } - if (imageWriterSettings != null) { - ImageWriterService.createImageWriter(imageId, imageWriterSettings); - } - newDataSources.add(newImage); - if (!StringUtils.isBlank(md5)) { - try { - newImage.setMD5(md5); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, String.format("Failed to add MD5 hash for image data source %s (objId=%d)", newImage.getName(), newImage.getId()), ex); - errorMessages.add(ex.getMessage()); - criticalErrorOccurred = true; - } catch (TskDataException ignored) { - /* - * The only reasonable way for this to happen at - * present is through C/C++ processing of an EWF - * image, which is not an error. - */ - } - } - if (!StringUtils.isBlank(sha1)) { - try { - newImage.setSha1(sha1); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, String.format("Failed to add SHA1 hash for image data source %s (objId=%d)", newImage.getName(), newImage.getId()), ex); - errorMessages.add(ex.getMessage()); - criticalErrorOccurred = true; - } catch (TskDataException ignored) { - /* - * The only reasonable way for this to happen at - * present is through C/C++ processing of an EWF - * image, which is not an error. - */ - } - } - if (!StringUtils.isBlank(sha256)) { - try { - newImage.setSha256(sha256); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, String.format("Failed to add SHA256 for image data source %s (objId=%d)", newImage.getName(), newImage.getId()), ex); - errorMessages.add(ex.getMessage()); - criticalErrorOccurred = true; - } catch (TskDataException ignored) { - /* - * The only reasonable way for this to happen at - * present is through C/C++ processing of an EWF - * image, which is not an error. - */ - } - } - } else { - String errorMessage = String.format("Error commiting after adding image %s to the case database, no object id returned", imagePath); //NON-NLS - logger.log(Level.SEVERE, errorMessage); - errorMessages.add(errorMessage); - criticalErrorOccurred = true; - } + newImage.setSha1(imageDetails.sha1); } catch (TskCoreException ex) { - logger.log(Level.SEVERE, String.format("Error committing adding image %s to the case database", imagePath), ex); //NON-NLS + logger.log(Level.SEVERE, String.format("Failed to add SHA1 hash for image data source %s (objId=%d)", newImage.getName(), newImage.getId()), ex); errorMessages.add(ex.getMessage()); criticalErrorOccurred = true; + } catch (TskDataException ignored) { + /* + * The only reasonable way for this to happen at + * present is through C/C++ processing of an EWF + * image, which is not an error. + */ + } + } + if (!StringUtils.isBlank(imageDetails.sha256)) { + try { + newImage.setSha256(imageDetails.sha256); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, String.format("Failed to add SHA256 for image data source %s (objId=%d)", newImage.getName(), newImage.getId()), ex); + errorMessages.add(ex.getMessage()); + criticalErrorOccurred = true; + } catch (TskDataException ignored) { + /* + * The only reasonable way for this to happen at + * present is through C/C++ processing of an EWF + * image, which is not an error. + */ } } } @@ -352,4 +305,37 @@ class AddImageTask implements Runnable { } } + /** + * Utility class to hold image data. + */ + static class ImageDetails { + String deviceId; + Image image; + int sectorSize; + String timeZone; + boolean ignoreFatOrphanFiles; + String md5; + String sha1; + String sha256; + ImageWriterSettings imageWriterSettings; + + ImageDetails(String deviceId, Image image, int sectorSize, String timeZone, boolean ignoreFatOrphanFiles, String md5, String sha1, String sha256, ImageWriterSettings imageWriterSettings) { + this.deviceId = deviceId; + this.image = image; + this.sectorSize = sectorSize; + this.timeZone = timeZone; + this.ignoreFatOrphanFiles = ignoreFatOrphanFiles; + this.md5 = md5; + this.sha1 = sha1; + this.sha256 = sha256; + this.imageWriterSettings = imageWriterSettings; + } + + String getImagePath() { + if (image.getPaths().length > 0) { + return image.getPaths()[0]; + } + return "Unknown data source path"; + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTaskCallback.java b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTaskCallback.java new file mode 100644 index 0000000000..ec76166419 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTaskCallback.java @@ -0,0 +1,38 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit 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.casemodule; + +import java.util.List; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback.DataSourceProcessorResult; +import org.sleuthkit.datamodel.Content; + +/** + * Called on completion of the add image task. + */ +interface AddImageTaskCallback { + + /** + * Called when the add image task is completed. + * + * @param result The result from the data source processor. + * @param errList The list of errors. + * @param newDataSources The list of new data sources. + */ + void onCompleted(DataSourceProcessorResult result, List errList, List newDataSources); +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardAddingProgressPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardAddingProgressPanel.java index 15eb0c3c67..2b880093e9 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardAddingProgressPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardAddingProgressPanel.java @@ -302,7 +302,9 @@ class AddImageWizardAddingProgressPanel extends ShortcutWizardDescriptorPanel { private void startIngest() { if (!newContents.isEmpty() && readyToIngest && !ingested) { ingested = true; - IngestManager.getInstance().queueIngestJob(newContents, ingestJobSettings); + if (dsProcessor != null && ! dsProcessor.supportsIngestStream()) { + IngestManager.getInstance().queueIngestJob(newContents, ingestJobSettings); + } setStateFinished(); } } @@ -360,8 +362,12 @@ class AddImageWizardAddingProgressPanel extends ShortcutWizardDescriptorPanel { setStateStarted(); - // Kick off the DSProcessor - dsProcessor.run(getDSPProgressMonitorImpl(), cbObj); + // Kick off the DSProcessor + if (dsProcessor.supportsIngestStream()) { + dsProcessor.runWithIngestStream(ingestJobSettings, getDSPProgressMonitorImpl(), cbObj); + } else { + dsProcessor.run(getDSPProgressMonitorImpl(), cbObj); + } } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardIterator.java b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardIterator.java index 9dad21446e..045bf775b2 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardIterator.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardIterator.java @@ -41,6 +41,7 @@ class AddImageWizardIterator implements WizardDescriptor.Iterator sleuthkit 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.casemodule; + +import java.util.List; +import org.sleuthkit.autopsy.ingest.IngestStream; +import org.sleuthkit.autopsy.ingest.IngestStreamClosedException; + +/** + * This is a default ingest stream to use with the data source processors when + * an IngestStream is not supplied. Adding files/data sources are no-ops. + */ +class DefaultIngestStream implements IngestStream { + + private boolean isClosed = false; + private boolean isStopped = false; + + @Override + public void addFiles(List fileObjectIds) throws IngestStreamClosedException { + // Do nothing + } + + @Override + public synchronized boolean isClosed() { + return isClosed; + } + + @Override + public synchronized void close() { + isClosed = true; + } + + @Override + public synchronized void stop() { + isClosed = true; + isStopped = true; + } + + @Override + public synchronized boolean wasStopped() { + return isStopped; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java index 596272f09f..9dd9a39fd4 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java @@ -24,6 +24,7 @@ import javax.swing.JPanel; import java.util.ArrayList; import java.util.Calendar; import java.util.List; +import java.util.logging.Level; import java.util.UUID; import javax.swing.filechooser.FileFilter; import org.openide.util.NbBundle; @@ -33,7 +34,14 @@ import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgress import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; import org.sleuthkit.autopsy.coreutils.DataSourceUtils; +import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor; +import org.sleuthkit.autopsy.ingest.IngestJobSettings; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.IngestStream; +import org.sleuthkit.datamodel.Image; +import org.sleuthkit.datamodel.SleuthkitJNI; +import org.sleuthkit.datamodel.TskCoreException; /** * A image file data source processor that implements the DataSourceProcessor @@ -49,6 +57,7 @@ import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor; public class ImageDSProcessor implements DataSourceProcessor, AutoIngestDataSourceProcessor { private final static String DATA_SOURCE_TYPE = NbBundle.getMessage(ImageDSProcessor.class, "ImageDSProcessor.dsType.text"); + private final Logger logger = Logger.getLogger(ImageDSProcessor.class.getName()); private static final List allExt = new ArrayList<>(); private static final GeneralFilter rawFilter = new GeneralFilter(GeneralFilter.RAW_IMAGE_EXTS, GeneralFilter.RAW_IMAGE_DESC); private static final GeneralFilter encaseFilter = new GeneralFilter(GeneralFilter.ENCASE_IMAGE_EXTS, GeneralFilter.ENCASE_IMAGE_DESC); @@ -58,6 +67,8 @@ public class ImageDSProcessor implements DataSourceProcessor, AutoIngestDataSour private static final List filtersList = new ArrayList<>(); private final ImageFilePanel configPanel; private AddImageTask addImageTask; + private IngestStream ingestStream = null; + private Image image = null; /* * TODO: Remove the setDataSourceOptionsCalled flag and the settings fields * when the deprecated method setDataSourceOptions is removed. @@ -170,6 +181,77 @@ public class ImageDSProcessor implements DataSourceProcessor, AutoIngestDataSour */ @Override public void run(DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { + ingestStream = new DefaultIngestStream(); + readConfigSettings(); + try { + image = SleuthkitJNI.addImageToDatabase(Case.getCurrentCase().getSleuthkitCase(), + new String[]{imagePath}, sectorSize, timeZone, md5, sha1, sha256, deviceId); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error adding data source with path " + imagePath + " to database", ex); + final List errors = new ArrayList<>(); + errors.add(ex.getMessage()); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errors, new ArrayList<>()); + return; + } + + doAddImageProcess(deviceId, imagePath, sectorSize, timeZone, ignoreFatOrphanFiles, md5, sha1, sha256, progressMonitor, callback); + } + + /** + * Adds a data source to the case database using a background task in a + * separate thread and the settings provided by the selection and + * configuration panel. Files found during ingest will be sent directly to the + * IngestStream provided. Returns as soon as the background task is started. + * The background task uses a callback object to signal task completion and + * return results. + * + * This method should not be called unless isPanelValid returns true, and + * should only be called for DSPs that support ingest streams. + * + * @param settings The ingest job settings. + * @param progress Progress monitor that will be used by the + * background task to report progress. + * @param callBack Callback that will be used by the background task + * to return results. + */ + @Override + public void runWithIngestStream(IngestJobSettings settings, DataSourceProcessorProgressMonitor progress, + DataSourceProcessorCallback callBack) { + + // Read the settings from the wizard + readConfigSettings(); + + // Set up the data source before creating the ingest stream + try { + image = SleuthkitJNI.addImageToDatabase(Case.getCurrentCase().getSleuthkitCase(), + new String[]{imagePath}, sectorSize, timeZone, md5, sha1, sha256, deviceId); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error adding data source with path " + imagePath + " to database", ex); + final List errors = new ArrayList<>(); + errors.add(ex.getMessage()); + callBack.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errors, new ArrayList<>()); + return; + } + + // Now initialize the ingest stream + try { + ingestStream = IngestManager.getInstance().openIngestStream(image, settings); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error starting ingest modules", ex); + final List errors = new ArrayList<>(); + errors.add(ex.getMessage()); + callBack.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errors, new ArrayList<>()); + return; + } + + + doAddImageProcess(deviceId, imagePath, sectorSize, timeZone, ignoreFatOrphanFiles, md5, sha1, sha256, progress, callBack); + } + + /** + * Store the options from the config panel. + */ + private void readConfigSettings() { if (!setDataSourceOptionsCalled) { configPanel.storeSettings(); deviceId = UUID.randomUUID().toString(); @@ -190,8 +272,17 @@ public class ImageDSProcessor implements DataSourceProcessor, AutoIngestDataSour sha256 = null; } } - run(deviceId, imagePath, sectorSize, timeZone, ignoreFatOrphanFiles, md5, sha1, sha256, progressMonitor, callback); } + + /** + * Check if this DSP supports ingest streams. + * + * @return True if this DSP supports an ingest stream, false otherwise. + */ + @Override + public boolean supportsIngestStream() { + return true; + } /** * Adds a data source to the case database using a background task in a @@ -215,7 +306,19 @@ public class ImageDSProcessor implements DataSourceProcessor, AutoIngestDataSour * @param callback Callback to call when processing is done. */ public void run(String deviceId, String imagePath, String timeZone, boolean ignoreFatOrphanFiles, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { - run(deviceId, imagePath, 0, timeZone, ignoreFatOrphanFiles, null, null, null, progressMonitor, callback); + ingestStream = new DefaultIngestStream(); + try { + image = SleuthkitJNI.addImageToDatabase(Case.getCurrentCase().getSleuthkitCase(), + new String[]{imagePath}, sectorSize, timeZone, "", "", "", deviceId); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error adding data source with path " + imagePath + " to database", ex); + final List errors = new ArrayList<>(); + errors.add(ex.getMessage()); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errors, new ArrayList<>()); + return; + } + + doAddImageProcess(deviceId, imagePath, 0, timeZone, ignoreFatOrphanFiles, null, null, null, progressMonitor, callback); } /** @@ -224,6 +327,10 @@ public class ImageDSProcessor implements DataSourceProcessor, AutoIngestDataSour * selection and configuration panel. Returns as soon as the background task * is started and uses the callback object to signal task completion and * return results. + * + * The image should be loaded in the database and stored in "image" + * before calling this method. Additionally, an ingest stream should be initialized + * and stored in "ingestStream". * * @param deviceId An ASCII-printable identifier for the device * associated with the data source that is @@ -243,8 +350,31 @@ public class ImageDSProcessor implements DataSourceProcessor, AutoIngestDataSour * during processing. * @param callback Callback to call when processing is done. */ - private void run(String deviceId, String imagePath, int sectorSize, String timeZone, boolean ignoreFatOrphanFiles, String md5, String sha1, String sha256, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { - addImageTask = new AddImageTask(deviceId, imagePath, sectorSize, timeZone, ignoreFatOrphanFiles, md5, sha1, sha256, null, progressMonitor, callback); + private void doAddImageProcess(String deviceId, String imagePath, int sectorSize, String timeZone, boolean ignoreFatOrphanFiles, String md5, String sha1, String sha256, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { + + // If the data source or ingest stream haven't been initialized, stop processing + if (ingestStream == null) { + String message = "Ingest stream was not initialized before running the add image process on " + imagePath; + logger.log(Level.SEVERE, message); + final List errors = new ArrayList<>(); + errors.add(message); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errors, new ArrayList<>()); + return; + } + if (image == null) { + String message = "Image was not added to database before running the add image process on " + imagePath; + logger.log(Level.SEVERE, message); + final List errors = new ArrayList<>(); + errors.add(message); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errors, new ArrayList<>()); + return; + } + + AddImageTask.ImageDetails imageDetails = new AddImageTask.ImageDetails(deviceId, image, sectorSize, timeZone, ignoreFatOrphanFiles, md5, sha1, sha256, null); + addImageTask = new AddImageTask(imageDetails, + progressMonitor, + new StreamingAddDataSourceCallbacks(ingestStream), + new StreamingAddImageTaskCallback(ingestStream, callback)); new Thread(addImageTask).start(); } @@ -260,6 +390,9 @@ public class ImageDSProcessor implements DataSourceProcessor, AutoIngestDataSour if (null != addImageTask) { addImageTask.cancelTask(); } + if (ingestStream != null) { + ingestStream.stop(); + } } /** @@ -316,7 +449,20 @@ public class ImageDSProcessor implements DataSourceProcessor, AutoIngestDataSour this.timeZone = Calendar.getInstance().getTimeZone().getID(); this.ignoreFatOrphanFiles = false; setDataSourceOptionsCalled = true; - run(deviceId, dataSourcePath.toString(), sectorSize, timeZone, ignoreFatOrphanFiles, null, null, null, progressMonitor, callBack); + + ingestStream = new DefaultIngestStream(); + try { + image = SleuthkitJNI.addImageToDatabase(Case.getCurrentCase().getSleuthkitCase(), + new String[]{imagePath}, sectorSize, timeZone, "", "", "", deviceId); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error adding data source with path " + imagePath + " to database", ex); + final List errors = new ArrayList<>(); + errors.add(ex.getMessage()); + callBack.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errors, new ArrayList<>()); + return; + } + + doAddImageProcess(deviceId, dataSourcePath.toString(), sectorSize, timeZone, ignoreFatOrphanFiles, null, null, null, progressMonitor, callBack); } /** diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/IngestJobInfoPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/IngestJobInfoPanel.java index b1e6d83482..6381acd32d 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/IngestJobInfoPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/IngestJobInfoPanel.java @@ -31,7 +31,9 @@ import javax.swing.JOptionPane; import javax.swing.event.ListSelectionEvent; import javax.swing.table.AbstractTableModel; import org.openide.util.NbBundle.Messages; +import static org.sleuthkit.autopsy.casemodule.Case.Events.CURRENT_CASE; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.datamodel.IngestJobInfo; import org.sleuthkit.datamodel.IngestModuleInfo; @@ -47,6 +49,8 @@ public final class IngestJobInfoPanel extends javax.swing.JPanel { private static final Logger logger = Logger.getLogger(IngestJobInfoPanel.class.getName()); private static final Set INGEST_JOB_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestJobEvent.STARTED, IngestManager.IngestJobEvent.CANCELLED, IngestManager.IngestJobEvent.COMPLETED); + private static final Set CASE_EVENTS_OF_INTEREST = EnumSet.of(Case.Events.CURRENT_CASE); + private List ingestJobs; private final List ingestJobsForSelectedDataSource = new ArrayList<>(); private IngestJobTableModel ingestJobTableModel = new IngestJobTableModel(); @@ -79,6 +83,16 @@ public final class IngestJobInfoPanel extends javax.swing.JPanel { refresh(); } }); + + Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, (PropertyChangeEvent evt) -> { + if (!(evt instanceof AutopsyEvent) || (((AutopsyEvent) evt).getSourceType() != AutopsyEvent.SourceType.LOCAL)) { + return; + } + + if (CURRENT_CASE == Case.Events.valueOf(evt.getPropertyName())) { + refresh(); + } + }); } /** @@ -110,9 +124,15 @@ public final class IngestJobInfoPanel extends javax.swing.JPanel { */ private void refresh() { try { - SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase(); - this.ingestJobs = skCase.getIngestJobs(); - setDataSource(selectedDataSource); + if (Case.isCaseOpen()) { + SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase(); + this.ingestJobs = skCase.getIngestJobs(); + setDataSource(selectedDataSource); + } else { + this.ingestJobs = new ArrayList<>(); + setDataSource(null); + } + } catch (TskCoreException | NoCurrentCaseException ex) { logger.log(Level.SEVERE, "Failed to load ingest jobs.", ex); JOptionPane.showMessageDialog(this, Bundle.IngestJobInfoPanel_loadIngestJob_error_text(), Bundle.IngestJobInfoPanel_loadIngestJob_error_title(), JOptionPane.ERROR_MESSAGE); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java index 60c0aed94b..0551d15466 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java @@ -18,15 +18,22 @@ */ package org.sleuthkit.autopsy.casemodule; +import java.util.ArrayList; import java.util.Calendar; +import java.util.List; import java.util.UUID; +import java.util.logging.Level; import javax.swing.JPanel; import org.openide.util.NbBundle; import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; +import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.imagewriter.ImageWriterSettings; +import org.sleuthkit.datamodel.Image; +import org.sleuthkit.datamodel.SleuthkitJNI; +import org.sleuthkit.datamodel.TskCoreException; /** * A local drive data source processor that implements the DataSourceProcessor @@ -37,6 +44,7 @@ import org.sleuthkit.autopsy.imagewriter.ImageWriterSettings; @ServiceProvider(service = DataSourceProcessor.class) public class LocalDiskDSProcessor implements DataSourceProcessor { + private final Logger logger = Logger.getLogger(LocalDiskDSProcessor.class.getName()); private static final String DATA_SOURCE_TYPE = NbBundle.getMessage(LocalDiskDSProcessor.class, "LocalDiskDSProcessor.dsType.text"); private final LocalDiskPanel configPanel; private AddImageTask addDiskTask; @@ -139,7 +147,25 @@ public class LocalDiskDSProcessor implements DataSourceProcessor { imageWriterSettings = null; } } - addDiskTask = new AddImageTask(deviceId, drivePath, sectorSize, timeZone, ignoreFatOrphanFiles, null, null, null, imageWriterSettings, progressMonitor, callback); + + Image image; + try { + image = SleuthkitJNI.addImageToDatabase(Case.getCurrentCase().getSleuthkitCase(), + new String[]{drivePath}, sectorSize, + timeZone, null, null, null, deviceId); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error adding local disk with path " + drivePath + " to database", ex); + final List errors = new ArrayList<>(); + errors.add(ex.getMessage()); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errors, new ArrayList<>()); + return; + } + + addDiskTask = new AddImageTask( + new AddImageTask.ImageDetails(deviceId, image, sectorSize, timeZone, ignoreFatOrphanFiles, null, null, null, imageWriterSettings), + progressMonitor, + new StreamingAddDataSourceCallbacks(new DefaultIngestStream()), + new StreamingAddImageTaskCallback(new DefaultIngestStream(), callback)); new Thread(addDiskTask).start(); } @@ -191,7 +217,23 @@ public class LocalDiskDSProcessor implements DataSourceProcessor { * @param callback Callback to call when processing is done. */ private void run(String deviceId, String drivePath, int sectorSize, String timeZone, boolean ignoreFatOrphanFiles, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { - addDiskTask = new AddImageTask(deviceId, drivePath, sectorSize, timeZone, ignoreFatOrphanFiles, null, null, null, imageWriterSettings, progressMonitor, callback); + Image image; + try { + image = SleuthkitJNI.addImageToDatabase(Case.getCurrentCase().getSleuthkitCase(), + new String[]{drivePath}, sectorSize, + timeZone, null, null, null, deviceId); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error adding local disk with path " + drivePath + " to database", ex); + final List errors = new ArrayList<>(); + errors.add(ex.getMessage()); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errors, new ArrayList<>()); + return; + } + + addDiskTask = new AddImageTask(new AddImageTask.ImageDetails(deviceId, image, sectorSize, timeZone, ignoreFatOrphanFiles, null, null, null, imageWriterSettings), + progressMonitor, + new StreamingAddDataSourceCallbacks(new DefaultIngestStream()), + new StreamingAddImageTaskCallback(new DefaultIngestStream(), callback)); new Thread(addDiskTask).start(); } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/StreamingAddDataSourceCallbacks.java b/Core/src/org/sleuthkit/autopsy/casemodule/StreamingAddDataSourceCallbacks.java new file mode 100644 index 0000000000..fb26c7a195 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/StreamingAddDataSourceCallbacks.java @@ -0,0 +1,66 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit 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.casemodule; + +import java.util.List; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.ingest.IngestStream; +import org.sleuthkit.autopsy.ingest.IngestStreamClosedException; +import org.sleuthkit.datamodel.AddDataSourceCallbacks; + +/** + * A set of callbacks to be called during the process of adding a data source to + * the case database. This implementation of the interface is suitable for + * streaming ingest use cases. + */ +class StreamingAddDataSourceCallbacks implements AddDataSourceCallbacks { + + private final Logger logger = Logger.getLogger(StreamingAddDataSourceCallbacks.class.getName()); + private final IngestStream ingestStream; + + /** + * Constructs a set of callbacks to be called during the process of adding a + * data source to the case database. This implementation of the interface is + * suitable for streaming ingest use cases. + * + * @param stream The IngestStream to send data to + */ + StreamingAddDataSourceCallbacks(IngestStream stream) { + ingestStream = stream; + } + + @Override + public void onFilesAdded(List fileObjectIds) { + if (ingestStream.wasStopped()) { + return; + } + + try { + ingestStream.addFiles(fileObjectIds); + } catch (IngestStreamClosedException ex) { + if (!ingestStream.wasStopped()) { + // If the ingest stream is closed but not stopped log the error. + // This state should only happen once the data source is completely + // added which means it's a severe error that files are still being added. + logger.log(Level.SEVERE, "Error adding files to ingest stream - ingest stream is closed"); + } + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/StreamingAddImageTaskCallback.java b/Core/src/org/sleuthkit/autopsy/casemodule/StreamingAddImageTaskCallback.java new file mode 100644 index 0000000000..d03f85097a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/StreamingAddImageTaskCallback.java @@ -0,0 +1,65 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit 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.casemodule; + +import java.util.List; +import org.sleuthkit.autopsy.ingest.IngestStream; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback.DataSourceProcessorResult; +import org.sleuthkit.datamodel.Content; + +/** + * A callback to be called on completion of an add image task. This + * implementation of the interface is suitable for streaming ingest use cases. + * It closes the ingest stream and then calls the data source processor done + * callback. + */ +class StreamingAddImageTaskCallback implements AddImageTaskCallback { + + private final IngestStream ingestStream; + private final DataSourceProcessorCallback dspCallback; + + /** + * Constructs a callback to be called on completion of an add image task. + * This implementation of the interface is suitable for streaming ingest use + * cases. It closes the ingest stream and then calls the data source + * processor done callback. + * + * @param ingestStream The ingest stream that data is being sent to. + * @param dspCallback The callback for non-ingest stream related + * processing. + */ + StreamingAddImageTaskCallback(IngestStream ingestStream, DataSourceProcessorCallback dspCallback) { + this.ingestStream = ingestStream; + this.dspCallback = dspCallback; + } + + /** + * Called when the add image task is completed. + * + * @param result The result from the data source processor. + * @param errList The list of errors. + * @param newDataSources The list of new data sources. + */ + @Override + public void onCompleted(DataSourceProcessorResult result, List errList, List newDataSources) { + ingestStream.close(); + dspCallback.done(result, errList, newDataSources); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourceSummary/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties-MERGED old mode 100755 new mode 100644 similarity index 96% rename from Core/src/org/sleuthkit/autopsy/casemodule/datasourceSummary/Bundle.properties-MERGED rename to Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties-MERGED index fe998b0e57..eb7affe9ee --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourceSummary/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties-MERGED @@ -1,4 +1,6 @@ CTL_DataSourceSummaryAction=Data Source Summary +DataSourceSummaryCountsPanel.ArtifactCountsTableModel.count.header=Count +DataSourceSummaryCountsPanel.ArtifactCountsTableModel.type.header=Result Type DataSourceSummaryCountsPanel.FilesByCategoryTableModel.all.row=All DataSourceSummaryCountsPanel.FilesByCategoryTableModel.allocated.row=Allocated DataSourceSummaryCountsPanel.FilesByCategoryTableModel.count.header=Count diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourceSummary/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle_ja.properties similarity index 100% rename from Core/src/org/sleuthkit/autopsy/casemodule/datasourceSummary/Bundle_ja.properties rename to Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle_ja.properties diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceInfoUtilities.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceInfoUtilities.java index c2d8263056..dcbf9bb82d 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceInfoUtilities.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceInfoUtilities.java @@ -18,7 +18,8 @@ */ package org.sleuthkit.autopsy.casemodule.datasourcesummary; -import java.util.ArrayList; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.List; import java.util.Map; import java.util.logging.Level; @@ -43,6 +44,297 @@ final class DataSourceInfoUtilities { private static final Logger logger = Logger.getLogger(DataSourceInfoUtilities.class.getName()); + /** + * Gets a count of files for a particular datasource where it is not a + * virtual directory and has a name. + * + * @param currentDataSource The datasource. + * @param additionalWhere Additional sql where clauses. + * @param onError The message to log on error. + * + * @return The count of files or null on error. + */ + private static Long getCountOfFiles(DataSource currentDataSource, String additionalWhere, String onError) { + if (currentDataSource != null) { + try { + SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase(); + return skCase.countFilesWhere( + "dir_type<>" + TskData.TSK_FS_NAME_TYPE_ENUM.VIRT_DIR.getValue() + + " AND name<>''" + + " AND data_source_obj_id=" + currentDataSource.getId() + + " AND " + additionalWhere); + } catch (TskCoreException | NoCurrentCaseException ex) { + logger.log(Level.WARNING, onError, ex); + //unable to get count of files for the specified types cell will be displayed as empty + } + } + return null; + } + + /** + * Get count of files in a data source. + * + * @param currentDataSource The data source. + * + * @return The count. + */ + static Long getCountOfFiles(DataSource currentDataSource) { + return getCountOfFiles(currentDataSource, + "type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType(), + "Unable to get count of files, providing empty results"); + } + + /** + * Get count of unallocated files in a data source. + * + * @param currentDataSource The data source. + * + * @return The count. + */ + static Long getCountOfUnallocatedFiles(DataSource currentDataSource) { + return getCountOfFiles(currentDataSource, + "type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType() + + " AND dir_flags=" + TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC.getValue(), + "Unable to get counts of unallocated files for datasource, providing empty results"); + } + + /** + * Get count of directories in a data source. + * + * @param currentDataSource The data source. + * + * @return The count. + */ + static Long getCountOfDirectories(DataSource currentDataSource) { + return getCountOfFiles(currentDataSource, + "type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType() + + " AND meta_type=" + TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR.getValue(), + "Unable to get count of directories for datasource, providing empty results"); + } + + /** + * Get count of slack files in a data source. + * + * @param currentDataSource The data source. + * + * @return The count. + */ + static Long getCountOfSlackFiles(DataSource currentDataSource) { + return getCountOfFiles(currentDataSource, + "type=" + TskData.TSK_DB_FILES_TYPE_ENUM.SLACK.getFileType(), + "Unable to get count of slack files for datasources, providing empty results"); + } + + /** + * An interface for handling a result set and returning a value. + */ + private interface ResultSetHandler { + + T process(ResultSet resultset) throws SQLException; + } + + /** + * Retrieves a result based on the provided query. + * + * @param query The query. + * @param processor The result set handler. + * @param errorMessage The error message to display if there is an error + * retrieving the resultset. + * + * @return The ResultSetHandler value or null if no ResultSet could be + * obtained. + */ + private static T getBaseQueryResult(String query, ResultSetHandler processor, String errorMessage) { + try (SleuthkitCase.CaseDbQuery dbQuery = Case.getCurrentCaseThrows().getSleuthkitCase().executeQuery(query)) { + ResultSet resultSet = dbQuery.getResultSet(); + try { + return processor.process(resultSet); + } catch (SQLException ex) { + logger.log(Level.WARNING, errorMessage, ex); + } + } catch (TskCoreException | NoCurrentCaseException ex) { + logger.log(Level.WARNING, errorMessage, ex); + } + return null; + } + + /** + * Gets the size of unallocated files in a particular datasource. + * + * @param currentDataSource The data source. + * + * @return The size or null if the query could not be executed. + */ + static Long getSizeOfUnallocatedFiles(DataSource currentDataSource) { + if (currentDataSource == null) { + return null; + } + + final String valueParam = "value"; + final String countParam = "count"; + String query = "SELECT SUM(size) AS " + valueParam + ", COUNT(*) AS " + countParam + + " FROM tsk_files WHERE type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType() + + " AND dir_type<>" + TskData.TSK_FS_NAME_TYPE_ENUM.VIRT_DIR.getValue() + + " AND dir_flags=" + TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC.getValue() + + " AND name<>''" + + " AND data_source_obj_id=" + currentDataSource.getId(); + + ResultSetHandler handler = (resultSet) -> { + if (resultSet.next()) { + // ensure that there is an unallocated count result that is attached to this data source + long resultCount = resultSet.getLong(valueParam); + return (resultCount > 0) ? resultSet.getLong(valueParam) : null; + } else { + return null; + } + }; + String errorMessage = "Unable to get size of unallocated files; returning null."; + + return getBaseQueryResult(query, handler, errorMessage); + } + + /** + * Retrieves counts for each artifact type in a data source. + * + * @param selectedDataSource The data source. + * + * @return A mapping of artifact type name to the counts or null if there + * was an error executing the query. + */ + static Map getCountsOfArtifactsByType(DataSource selectedDataSource) { + if (selectedDataSource == null) { + return Collections.emptyMap(); + } + + final String nameParam = "name"; + final String valueParam = "value"; + String query + = "SELECT bbt.display_name AS " + nameParam + ", COUNT(*) AS " + valueParam + + " FROM blackboard_artifacts bba " + + " INNER JOIN blackboard_artifact_types bbt ON bba.artifact_type_id = bbt.artifact_type_id" + + " WHERE bba.data_source_obj_id =" + selectedDataSource.getId() + + " GROUP BY bbt.display_name"; + + ResultSetHandler> handler = (resultSet) -> { + Map toRet = new HashMap<>(); + while (resultSet.next()) { + try { + toRet.put(resultSet.getString(nameParam), resultSet.getLong(valueParam)); + } catch (SQLException ex) { + logger.log(Level.WARNING, "Failed to get a result pair from the result set.", ex); + } + } + + return toRet; + }; + + String errorMessage = "Unable to get artifact type counts; returning null."; + + return getBaseQueryResult(query, handler, errorMessage); + } + + /** + * Generates a string which is a concatenation of the value received from + * the result set. + * + * @param query The query. + * @param valueParam The parameter for the value in the result set. + * @param separator The string separator used in concatenation. + * @param errorMessage The error message if the result set could not + * be received. + * @param singleErrorMessage The error message if a single result could not + * be obtained. + * + * @return The concatenated string or null if the query could not be + * executed. + */ + private static String getConcattedStringsResult(String query, String valueParam, String separator, String errorMessage, String singleErrorMessage) { + ResultSetHandler handler = (resultSet) -> { + String toRet = ""; + boolean first = true; + while (resultSet.next()) { + try { + if (first) { + first = false; + } else { + toRet += separator; + } + toRet += resultSet.getString(valueParam); + } catch (SQLException ex) { + logger.log(Level.WARNING, singleErrorMessage, ex); + } + } + + return toRet; + }; + + return getBaseQueryResult(query, handler, errorMessage); + } + + /** + * Generates a concatenated string value based on the text value of a + * particular attribute in a particular artifact for a specific data source. + * + * @param dataSourceId The data source id. + * @param artifactTypeId The artifact type id. + * @param attributeTypeId The attribute type id. + * + * @return The concatenated value or null if the query could not be + * executed. + */ + private static String getConcattedAttrValue(long dataSourceId, int artifactTypeId, int attributeTypeId) { + final String valueParam = "concatted_attribute_value"; + String query = "SELECT attr.value_text AS " + valueParam + + " FROM blackboard_artifacts bba " + + " INNER JOIN blackboard_attributes attr ON bba.artifact_id = attr.artifact_id " + + " WHERE bba.data_source_obj_id = " + dataSourceId + + " AND bba.artifact_type_id = " + artifactTypeId + + " AND attr.attribute_type_id = " + attributeTypeId; + + String errorMessage = "Unable to execute query to retrieve concatted attribute values."; + String singleErrorMessage = "There was an error retrieving one of the results. That result will be omitted from concatted value."; + String separator = ", "; + return getConcattedStringsResult(query, valueParam, separator, errorMessage, singleErrorMessage); + } + + /** + * Retrieves the concatenation of operating system attributes for a + * particular data source. + * + * @param dataSource The data source. + * + * @return The concatenated value or null if the query could not be + * executed. + */ + static String getOperatingSystems(DataSource dataSource) { + if (dataSource == null) { + return null; + } + + return getConcattedAttrValue(dataSource.getId(), + BlackboardArtifact.ARTIFACT_TYPE.TSK_OS_INFO.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME.getTypeID()); + } + + /** + * Retrieves the concatenation of data source usage for a particular data + * source. + * + * @param dataSource The data source. + * + * @return The concatenated value or null if the query could not be + * executed. + */ + static String getDataSourceType(DataSource dataSource) { + if (dataSource == null) { + return null; + } + + return getConcattedAttrValue(dataSource.getId(), + BlackboardArtifact.ARTIFACT_TYPE.TSK_DATA_SOURCE_USAGE.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DESCRIPTION.getTypeID()); + } + /** * Get a map containing the TSK_DATA_SOURCE_USAGE description attributes * associated with each data source in the current case. @@ -147,39 +439,6 @@ final class DataSourceInfoUtilities { } } - /** - * Get a map containing the names of operating systems joined in a comma - * seperated list to the Data Source they exist on in the current case. No - * item will exist in the map for data sources which do not contain - * TS_OS_INFO artifacts which have a program name. - * - * @return Collection which maps datasource id to a String which is a comma - * seperated list of Operating system names found on the data - * source. - */ - static Map getOperatingSystems() { - Map osDetailMap = new HashMap<>(); - try { - SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase(); - ArrayList osInfoArtifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_OS_INFO); - for (BlackboardArtifact osInfo : osInfoArtifacts) { - BlackboardAttribute programName = osInfo.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME)); - if (programName != null) { - String currentOsString = osDetailMap.get(osInfo.getDataSource().getId()); - if (currentOsString == null || currentOsString.isEmpty()) { - currentOsString = programName.getValueString(); - } else { - currentOsString = currentOsString + ", " + programName.getValueString(); - } - osDetailMap.put(osInfo.getDataSource().getId(), currentOsString); - } - } - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Failed to load OS info artifacts.", ex); - } - return osDetailMap; - } - /** * Get the number of files in the case database for the current data source * which have the specified mimetypes. @@ -212,134 +471,6 @@ final class DataSourceInfoUtilities { return null; } - /** - * Get a map containing the number of unallocated files in each data source - * in the current case. - * - * @return Collection which maps datasource id to a count for the number of - * unallocated files in the datasource, will only contain entries - * for datasources which have at least 1 unallocated file - */ - static Map getCountsOfUnallocatedFiles() { - try { - final String countUnallocatedFilesQuery = "data_source_obj_id, COUNT(*) AS value" - + " FROM tsk_files WHERE type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType() - + " AND dir_type<>" + TskData.TSK_FS_NAME_TYPE_ENUM.VIRT_DIR.getValue() - + " AND dir_flags=" + TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC.getValue() - + " AND name<>'' GROUP BY data_source_obj_id"; //NON-NLS - return getValuesMap(countUnallocatedFilesQuery); - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Unable to get counts of unallocated files for all datasources, providing empty results", ex); - return Collections.emptyMap(); - } - } - - /** - * Get a map containing the total size of unallocated files in each data - * source in the current case. - * - * @return Collection which maps datasource id to a total size in bytes of - * unallocated files in the datasource, will only contain entries - * for datasources which have at least 1 unallocated file - */ - static Map getSizeOfUnallocatedFiles() { - try { - final String countUnallocatedFilesQuery = "data_source_obj_id, sum(size) AS value" - + " FROM tsk_files WHERE type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType() - + " AND dir_type<>" + TskData.TSK_FS_NAME_TYPE_ENUM.VIRT_DIR.getValue() - + " AND dir_flags=" + TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC.getValue() - + " AND name<>'' GROUP BY data_source_obj_id"; //NON-NLS - return getValuesMap(countUnallocatedFilesQuery); - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Unable to get size of unallocated files for all datasources, providing empty results", ex); - return Collections.emptyMap(); - } - } - - /** - * Get a map containing the number of directories in each data source in the - * current case. - * - * @return Collection which maps datasource id to a count for the number of - * directories in the datasource, will only contain entries for - * datasources which have at least 1 directory - */ - static Map getCountsOfDirectories() { - try { - final String countDirectoriesQuery = "data_source_obj_id, COUNT(*) AS value" - + " FROM tsk_files WHERE type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType() - + " AND dir_type<>" + TskData.TSK_FS_NAME_TYPE_ENUM.VIRT_DIR.getValue() - + " AND meta_type=" + TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR.getValue() - + " AND name<>'' GROUP BY data_source_obj_id"; //NON-NLS - return getValuesMap(countDirectoriesQuery); - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Unable to get counts of directories for all datasources, providing empty results", ex); - return Collections.emptyMap(); - } - } - - /** - * Get a map containing the number of slack files in each data source in the - * current case. - * - * @return Collection which maps datasource id to a count for the number of - * slack files in the datasource, will only contain entries for - * datasources which have at least 1 slack file - */ - static Map getCountsOfSlackFiles() { - try { - final String countSlackFilesQuery = "data_source_obj_id, COUNT(*) AS value" - + " FROM tsk_files WHERE type=" + TskData.TSK_DB_FILES_TYPE_ENUM.SLACK.getFileType() - + " AND dir_type<>" + TskData.TSK_FS_NAME_TYPE_ENUM.VIRT_DIR.getValue() - + " AND name<>'' GROUP BY data_source_obj_id"; //NON-NLS - return getValuesMap(countSlackFilesQuery); - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Unable to get counts of slack files for all datasources, providing empty results", ex); - return Collections.emptyMap(); - } - } - - /** - * Get a map containing maps which map artifact type to the number of times - * it exosts in each data source in the current case. - * - * @return Collection which maps datasource id to maps of artifact display - * name to number of occurences in the datasource, will only contain - * entries for artifacts which have at least one occurence in the - * data source. - */ - static Map> getCountsOfArtifactsByType() { - try { - final String countArtifactsQuery = "blackboard_artifacts.data_source_obj_id, blackboard_artifact_types.display_name AS label, COUNT(*) AS value" - + " FROM blackboard_artifacts, blackboard_artifact_types" - + " WHERE blackboard_artifacts.artifact_type_id = blackboard_artifact_types.artifact_type_id" - + " GROUP BY blackboard_artifacts.data_source_obj_id, blackboard_artifact_types.display_name"; - return getLabeledValuesMap(countArtifactsQuery); - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Unable to get counts of all artifact types for all datasources, providing empty results", ex); - return Collections.emptyMap(); - } - } - - /** - * Helper method to execute a select query with a - * DataSourceLabeledValueCallback. - * - * @param query the portion of the query which should follow the SELECT - * - * @return a map of datasource object IDs to maps of String labels to the - * values associated with them. - * - * @throws TskCoreException - * @throws NoCurrentCaseException - */ - private static Map> getLabeledValuesMap(String query) throws TskCoreException, NoCurrentCaseException { - SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase(); - DataSourceLabeledValueCallback callback = new DataSourceLabeledValueCallback(); - skCase.getCaseDbAccessManager().select(query, callback); - return callback.getMapOfLabeledValues(); - } - /** * Helper method to execute a select query with a * DataSourceSingleValueCallback. diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryCountsPanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryCountsPanel.form index d3a17e0205..9a82746da6 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryCountsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryCountsPanel.form @@ -70,18 +70,13 @@ - - - - - - + @@ -93,25 +88,20 @@ - - - - - - + - + @@ -123,15 +113,6 @@ - - - - - - -
-
-
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryCountsPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryCountsPanel.java index 1d99f1c445..6bb6603c11 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryCountsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryCountsPanel.java @@ -18,15 +18,12 @@ */ package org.sleuthkit.autopsy.casemodule.datasourcesummary; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.JLabel; -import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; -import javax.swing.table.DefaultTableModel; import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.FileTypeUtils; import org.sleuthkit.datamodel.DataSource; @@ -34,69 +31,224 @@ import org.sleuthkit.datamodel.DataSource; * Panel for displaying summary information on the known files present in the * specified DataSource */ +@Messages({"DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.type.header=File Type", + "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.count.header=Count", + "DataSourceSummaryCountsPanel.ArtifactCountsTableModel.type.header=Result Type", + "DataSourceSummaryCountsPanel.ArtifactCountsTableModel.count.header=Count", + "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.type.header=File Type", + "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.count.header=Count" +}) class DataSourceSummaryCountsPanel extends javax.swing.JPanel { + // Result returned for a data model if no data found. + private static final Object[][] EMPTY_PAIRS = new Object[][]{}; + + // column headers for mime type table + private static final Object[] MIME_TYPE_COLUMN_HEADERS = new Object[]{ + Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_type_header(), + Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_count_header() + }; + + // column headers for file by category table + private static final Object[] FILE_BY_CATEGORY_COLUMN_HEADERS = new Object[]{ + Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_type_header(), + Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_count_header() + }; + + // column headers for artifact counts table + private static final Object[] ARTIFACT_COUNTS_COLUMN_HEADERS = new Object[]{ + Bundle.DataSourceSummaryCountsPanel_ArtifactCountsTableModel_type_header(), + Bundle.DataSourceSummaryCountsPanel_ArtifactCountsTableModel_count_header() + }; + private static final long serialVersionUID = 1L; - private FilesByMimeTypeTableModel filesByMimeTypeTableModel = new FilesByMimeTypeTableModel(null); - private FilesByCategoryTableModel filesByCategoryTableModel = new FilesByCategoryTableModel(null); private static final Logger logger = Logger.getLogger(DataSourceSummaryCountsPanel.class.getName()); - private final Map allFilesCountsMap; - private final Map slackFilesCountsMap; - private final Map directoriesCountsMap; - private final Map unallocatedFilesCountsMap; - private final Map> artifactsByTypeCountsMap; private final DefaultTableCellRenderer rightAlignedRenderer = new DefaultTableCellRenderer(); + private DataSource dataSource; + /** * Creates new form DataSourceSummaryCountsPanel */ - DataSourceSummaryCountsPanel(Map fileCountsMap) { - this.allFilesCountsMap = fileCountsMap; - this.slackFilesCountsMap = DataSourceInfoUtilities.getCountsOfSlackFiles(); - this.directoriesCountsMap = DataSourceInfoUtilities.getCountsOfDirectories(); - this.unallocatedFilesCountsMap = DataSourceInfoUtilities.getCountsOfUnallocatedFiles(); - this.artifactsByTypeCountsMap = DataSourceInfoUtilities.getCountsOfArtifactsByType(); + DataSourceSummaryCountsPanel() { rightAlignedRenderer.setHorizontalAlignment(JLabel.RIGHT); initComponents(); fileCountsByMimeTypeTable.getTableHeader().setReorderingAllowed(false); fileCountsByCategoryTable.getTableHeader().setReorderingAllowed(false); + artifactCountsTable.getTableHeader().setReorderingAllowed(false); + setDataSource(null); } /** - * Specify the DataSource to display file information for + * The datasource currently used as the model in this panel. * - * @param selectedDataSource the DataSource to display file information for + * @return The datasource currently being used as the model in this panel. */ - void updateCountsTableData(DataSource selectedDataSource) { - filesByMimeTypeTableModel = new FilesByMimeTypeTableModel(selectedDataSource); - fileCountsByMimeTypeTable.setModel(filesByMimeTypeTableModel); + public DataSource getDataSource() { + return dataSource; + } + + /** + * Sets datasource to visualize in the panel. + * + * @param dataSource The datasource to use in this panel. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + if (dataSource == null || !Case.isCaseOpen()) { + updateCountsTableData(EMPTY_PAIRS, + EMPTY_PAIRS, + EMPTY_PAIRS); + } else { + updateCountsTableData(getMimeTypeModel(dataSource), + getFileCategoryModel(dataSource), + getArtifactCountsModel(dataSource)); + } + + } + + /** + * Specify the DataSource to display file information for. + * + * @param mimeTypeDataModel The mime type data model. + * @param fileCategoryDataModel The file category data model. + * @param artifactDataModel The artifact type data model. + */ + private void updateCountsTableData(Object[][] mimeTypeDataModel, Object[][] fileCategoryDataModel, Object[][] artifactDataModel) { + fileCountsByMimeTypeTable.setModel(new NonEditableTableModel(mimeTypeDataModel, MIME_TYPE_COLUMN_HEADERS)); fileCountsByMimeTypeTable.getColumnModel().getColumn(1).setCellRenderer(rightAlignedRenderer); fileCountsByMimeTypeTable.getColumnModel().getColumn(0).setPreferredWidth(130); - filesByCategoryTableModel = new FilesByCategoryTableModel(selectedDataSource); - fileCountsByCategoryTable.setModel(filesByCategoryTableModel); + + fileCountsByCategoryTable.setModel(new NonEditableTableModel(fileCategoryDataModel, FILE_BY_CATEGORY_COLUMN_HEADERS)); fileCountsByCategoryTable.getColumnModel().getColumn(1).setCellRenderer(rightAlignedRenderer); fileCountsByCategoryTable.getColumnModel().getColumn(0).setPreferredWidth(130); - updateArtifactCounts(selectedDataSource); + + artifactCountsTable.setModel(new NonEditableTableModel(artifactDataModel, ARTIFACT_COUNTS_COLUMN_HEADERS)); + artifactCountsTable.getColumnModel().getColumn(0).setPreferredWidth(230); + artifactCountsTable.getColumnModel().getColumn(1).setCellRenderer(rightAlignedRenderer); + this.repaint(); } /** - * Helper method to update the artifact specific counts by clearing the - * table and adding counts for the artifacts which exist in the selected - * data source. + * Determines the JTable data model for datasource mime types. * - * @param selectedDataSource the data source to display artifact counts for + * @param dataSource The DataSource. + * + * @return The model to be used with a JTable. */ - private void updateArtifactCounts(DataSource selectedDataSource) { - ((DefaultTableModel) artifactCountsTable.getModel()).setRowCount(0); - if (selectedDataSource != null && artifactsByTypeCountsMap.get(selectedDataSource.getId()) != null) { - Map artifactCounts = artifactsByTypeCountsMap.get(selectedDataSource.getId()); - for (String key : artifactCounts.keySet()) { - ((DefaultTableModel) artifactCountsTable.getModel()).addRow(new Object[]{key, artifactCounts.get(key)}); - } + @Messages({ + "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.images.row=Images", + "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.videos.row=Videos", + "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.audio.row=Audio", + "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.documents.row=Documents", + "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.executables.row=Executables" + }) + private static Object[][] getMimeTypeModel(DataSource dataSource) { + return new Object[][]{ + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_images_row(), + getCount(dataSource, FileTypeUtils.FileTypeCategory.IMAGE)}, + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_videos_row(), + getCount(dataSource, FileTypeUtils.FileTypeCategory.VIDEO)}, + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_audio_row(), + getCount(dataSource, FileTypeUtils.FileTypeCategory.AUDIO)}, + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_documents_row(), + getCount(dataSource, FileTypeUtils.FileTypeCategory.DOCUMENTS)}, + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_executables_row(), + getCount(dataSource, FileTypeUtils.FileTypeCategory.EXECUTABLE)} + }; + } + + /** + * Retrieves the counts of files of a particular mime type for a particular + * DataSource. + * + * @param dataSource The DataSource. + * @param category The mime type category. + * + * @return The count. + */ + private static Long getCount(DataSource dataSource, FileTypeUtils.FileTypeCategory category) { + return DataSourceInfoUtilities.getCountOfFilesForMimeTypes(dataSource, category.getMediaTypes()); + } + + /** + * Determines the JTable data model for datasource file categories. + * + * @param dataSource The DataSource. + * + * @return The model to be used with a JTable. + */ + @Messages({ + "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.all.row=All", + "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.allocated.row=Allocated", + "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.unallocated.row=Unallocated", + "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.slack.row=Slack", + "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.directory.row=Directory" + }) + private static Object[][] getFileCategoryModel(DataSource selectedDataSource) { + Long fileCount = zeroIfNull(DataSourceInfoUtilities.getCountOfFiles(selectedDataSource)); + Long unallocatedFiles = zeroIfNull(DataSourceInfoUtilities.getCountOfUnallocatedFiles(selectedDataSource)); + Long allocatedFiles = zeroIfNull(getAllocatedCount(fileCount, unallocatedFiles)); + Long slackFiles = zeroIfNull(DataSourceInfoUtilities.getCountOfSlackFiles(selectedDataSource)); + Long directories = zeroIfNull(DataSourceInfoUtilities.getCountOfDirectories(selectedDataSource)); + + return new Object[][]{ + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_all_row(), fileCount}, + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_allocated_row(), allocatedFiles}, + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_unallocated_row(), unallocatedFiles}, + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_slack_row(), slackFiles}, + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_directory_row(), directories} + }; + } + + /** + * Returns 0 if value is null. + * + * @param origValue The original value. + * + * @return The value or 0 if null. + */ + private static Long zeroIfNull(Long origValue) { + return origValue == null ? 0 : origValue; + } + + /** + * Safely gets the allocated files count. + * + * @param allFilesCount The count of all files. + * @param unallocatedFilesCount The count of unallocated files. + * + * @return The count of allocated files. + */ + private static long getAllocatedCount(Long allFilesCount, Long unallocatedFilesCount) { + if (allFilesCount == null) { + return 0; + } else if (unallocatedFilesCount == null) { + return allFilesCount; + } else { + return allFilesCount - unallocatedFilesCount; } - artifactCountsTable.getColumnModel().getColumn(0).setPreferredWidth(230); - artifactCountsTable.getColumnModel().getColumn(1).setCellRenderer(rightAlignedRenderer); + } + + /** + * The counts of different artifact types found in a DataSource. + * + * @param selectedDataSource The DataSource. + * + * @return The JTable data model of counts of artifact types. + */ + private static Object[][] getArtifactCountsModel(DataSource selectedDataSource) { + Map artifactMapping = DataSourceInfoUtilities.getCountsOfArtifactsByType(selectedDataSource); + if (artifactMapping == null) { + return EMPTY_PAIRS; + } + + return artifactMapping.entrySet().stream() + .filter((entrySet) -> entrySet != null && entrySet.getKey() != null) + .sorted((a, b) -> a.getKey().compareTo(b.getKey())) + .map((entrySet) -> new Object[]{entrySet.getKey(), entrySet.getValue()}) + .toArray(Object[][]::new); } /** @@ -118,35 +270,16 @@ class DataSourceSummaryCountsPanel extends javax.swing.JPanel { artifactCountsScrollPane = new javax.swing.JScrollPane(); artifactCountsTable = new javax.swing.JTable(); - fileCountsByMimeTypeTable.setModel(filesByMimeTypeTableModel); fileCountsByMimeTypeScrollPane.setViewportView(fileCountsByMimeTypeTable); org.openide.awt.Mnemonics.setLocalizedText(byMimeTypeLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryCountsPanel.class, "DataSourceSummaryCountsPanel.byMimeTypeLabel.text")); // NOI18N - fileCountsByCategoryTable.setModel(filesByCategoryTableModel); fileCountsByCategoryScrollPane.setViewportView(fileCountsByCategoryTable); org.openide.awt.Mnemonics.setLocalizedText(byCategoryLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryCountsPanel.class, "DataSourceSummaryCountsPanel.byCategoryLabel.text")); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(DataSourceSummaryCountsPanel.class, "DataSourceSummaryCountsPanel.jLabel1.text")); // NOI18N - artifactCountsTable.setAutoCreateRowSorter(true); - artifactCountsTable.setModel(new javax.swing.table.DefaultTableModel( - new Object [][] { - - }, - new String [] { - "Result Type", "Count" - } - ) { - boolean[] canEdit = new boolean [] { - false, false - }; - - public boolean isCellEditable(int rowIndex, int columnIndex) { - return canEdit [columnIndex]; - } - }); artifactCountsScrollPane.setViewportView(artifactCountsTable); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); @@ -206,190 +339,4 @@ class DataSourceSummaryCountsPanel extends javax.swing.JPanel { private javax.swing.JTable fileCountsByMimeTypeTable; private javax.swing.JLabel jLabel1; // End of variables declaration//GEN-END:variables - - /** - * Table model for the files table model to display counts of specific file - * types by mime type found in the currently selected data source. - */ - @Messages({"DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.type.header=File Type", - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.count.header=Count"}) - private class FilesByMimeTypeTableModel extends AbstractTableModel { - - private static final long serialVersionUID = 1L; - private final DataSource currentDataSource; - private final List columnHeaders = new ArrayList<>(); - private static final int IMAGES_ROW_INDEX = 0; - private static final int VIDEOS_ROW_INDEX = 1; - private static final int AUDIO_ROW_INDEX = 2; - private static final int DOCUMENTS_ROW_INDEX = 3; - private static final int EXECUTABLES_ROW_INDEX = 4; - - /** - * Create a FilesByMimeTypeTableModel for the speicified datasource. - * - * @param selectedDataSource the datasource which this - * FilesByMimeTypeTablemodel will represent - */ - FilesByMimeTypeTableModel(DataSource selectedDataSource) { - columnHeaders.add(Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_type_header()); - columnHeaders.add(Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_count_header()); - currentDataSource = selectedDataSource; - } - - @Override - public int getRowCount() { - //should be kept equal to the number of types we are displaying in the tables - return 5; - } - - @Override - public int getColumnCount() { - return columnHeaders.size(); - } - - @Messages({ - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.images.row=Images", - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.videos.row=Videos", - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.audio.row=Audio", - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.documents.row=Documents", - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.executables.row=Executables" - }) - @Override - public Object getValueAt(int rowIndex, int columnIndex) { - if (columnIndex == 0) { - switch (rowIndex) { - case IMAGES_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_images_row(); - case VIDEOS_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_videos_row(); - case AUDIO_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_audio_row(); - case DOCUMENTS_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_documents_row(); - case EXECUTABLES_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_executables_row(); - default: - break; - } - } else if (columnIndex == 1) { - switch (rowIndex) { - case 0: - return DataSourceInfoUtilities.getCountOfFilesForMimeTypes(currentDataSource, FileTypeUtils.FileTypeCategory.IMAGE.getMediaTypes()); - case 1: - return DataSourceInfoUtilities.getCountOfFilesForMimeTypes(currentDataSource, FileTypeUtils.FileTypeCategory.VIDEO.getMediaTypes()); - case 2: - return DataSourceInfoUtilities.getCountOfFilesForMimeTypes(currentDataSource, FileTypeUtils.FileTypeCategory.AUDIO.getMediaTypes()); - case 3: - return DataSourceInfoUtilities.getCountOfFilesForMimeTypes(currentDataSource, FileTypeUtils.FileTypeCategory.DOCUMENTS.getMediaTypes()); - case 4: - return DataSourceInfoUtilities.getCountOfFilesForMimeTypes(currentDataSource, FileTypeUtils.FileTypeCategory.EXECUTABLE.getMediaTypes()); - default: - break; - } - } - return null; - } - - @Override - public String getColumnName(int column) { - return columnHeaders.get(column); - } - } - - /** - * Table model for the files table model to display counts of specific file - * types by category found in the currently selected data source. - */ - @Messages({"DataSourceSummaryCountsPanel.FilesByCategoryTableModel.type.header=File Type", - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.count.header=Count"}) - private class FilesByCategoryTableModel extends AbstractTableModel { - - private static final long serialVersionUID = 1L; - private final DataSource currentDataSource; - private final List columnHeaders = new ArrayList<>(); - private static final int ALL_FILES_ROW_INDEX = 0; - private static final int ALLOCATED_FILES_ROW_INDEX = 1; - private static final int UNALLOCATED_FILES_ROW_INDEX = 2; - private static final int SLACK_FILES_ROW_INDEX = 3; - private static final int DIRECTORIES_ROW_INDEX = 4; - /** - * Create a FilesByCategoryTableModel for the speicified datasource. - * - * @param selectedDataSource the datasource which this - * FilesByCategoryTablemodel will represent - */ - FilesByCategoryTableModel(DataSource selectedDataSource) { - columnHeaders.add(Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_type_header()); - columnHeaders.add(Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_count_header()); - currentDataSource = selectedDataSource; - } - - @Override - public int getRowCount() { - //should be kept equal to the number of types we are displaying in the tables - return 5; - } - - @Override - public int getColumnCount() { - return columnHeaders.size(); - } - - @Messages({ - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.all.row=All", - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.allocated.row=Allocated", - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.unallocated.row=Unallocated", - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.slack.row=Slack", - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.directory.row=Directory" - }) - @Override - public Object getValueAt(int rowIndex, int columnIndex) { - if (columnIndex == 0) { - switch (rowIndex) { - case ALL_FILES_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_all_row(); - case ALLOCATED_FILES_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_allocated_row(); - case UNALLOCATED_FILES_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_unallocated_row(); - case SLACK_FILES_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_slack_row(); - case DIRECTORIES_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_directory_row(); - default: - break; - } - } else if (columnIndex == 1 && currentDataSource != null) { - switch (rowIndex) { - case 0: - return allFilesCountsMap.get(currentDataSource.getId()) == null ? 0 : allFilesCountsMap.get(currentDataSource.getId()); - case 1: - //All files should be either allocated or unallocated as dir_flags only has two values so any file that isn't unallocated is allocated - Long unallocatedFilesCount = unallocatedFilesCountsMap.get(currentDataSource.getId()); - Long allFilesCount = allFilesCountsMap.get(currentDataSource.getId()); - if (allFilesCount == null) { - return 0; - } else if (unallocatedFilesCount == null) { - return allFilesCount; - } else { - return allFilesCount - unallocatedFilesCount; - } - case 2: - return unallocatedFilesCountsMap.get(currentDataSource.getId()) == null ? 0 : unallocatedFilesCountsMap.get(currentDataSource.getId()); - case 3: - return slackFilesCountsMap.get(currentDataSource.getId()) == null ? 0 : slackFilesCountsMap.get(currentDataSource.getId()); - case 4: - return directoriesCountsMap.get(currentDataSource.getId()) == null ? 0 : directoriesCountsMap.get(currentDataSource.getId()); - default: - break; - } - } - return null; - } - - @Override - public String getColumnName(int column) { - return columnHeaders.get(column); - } - } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDetailsPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDetailsPanel.java index 1b898f4995..21caa2c4b8 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDetailsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDetailsPanel.java @@ -19,12 +19,12 @@ package org.sleuthkit.autopsy.casemodule.datasourcesummary; import java.text.DecimalFormat; -import java.util.Map; -import java.util.HashMap; import java.util.logging.Level; import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.table.DefaultTableModel; +import org.apache.commons.lang3.StringUtils; import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.Image; import org.sleuthkit.datamodel.TskCoreException; @@ -36,23 +36,47 @@ class DataSourceSummaryDetailsPanel extends javax.swing.JPanel { //Because this panel was made using the gridbaglayout and netbean's Customize Layout tool it will be best to continue to modify it through that private static final long serialVersionUID = 1L; - private Map osDetailMap = new HashMap<>(); private static final Integer SIZE_COVERSION_CONSTANT = 1000; private static final DecimalFormat APPROXIMATE_SIZE_FORMAT = new DecimalFormat("#.##"); - private final Map unallocatedFilesSizeMap; - private final Map usageMap; private static final Logger logger = Logger.getLogger(DataSourceSummaryDetailsPanel.class.getName()); + private DataSource dataSource; + /** * Creates new form DataSourceSummaryDetailsPanel */ @Messages({"DataSourceSummaryDetailsPanel.getDataSources.error.text=Failed to get the list of datasources for the current case.", "DataSourceSummaryDetailsPanel.getDataSources.error.title=Load Failure"}) - DataSourceSummaryDetailsPanel(Map usageMap) { + DataSourceSummaryDetailsPanel() { initComponents(); - this.usageMap = usageMap; - this.unallocatedFilesSizeMap = DataSourceInfoUtilities.getSizeOfUnallocatedFiles(); - osDetailMap = DataSourceInfoUtilities.getOperatingSystems(); + setDataSource(null); + } + + /** + * The datasource currently used as the model in this panel. + * + * @return The datasource currently being used as the model in this panel. + */ + public DataSource getDataSource() { + return dataSource; + } + + /** + * Sets datasource to visualize in the panel. + * + * @param dataSource The datasource to use in this panel. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + + if (dataSource == null || !Case.isCaseOpen()) { + updateDetailsPanelData(null, null, null, null); + } else { + updateDetailsPanelData(dataSource, + DataSourceInfoUtilities.getSizeOfUnallocatedFiles(dataSource), + DataSourceInfoUtilities.getOperatingSystems(dataSource), + DataSourceInfoUtilities.getDataSourceType(dataSource)); + } } /** @@ -60,77 +84,80 @@ class DataSourceSummaryDetailsPanel extends javax.swing.JPanel { * * @param selectedDataSource the DataSource to display details about. */ - void updateDetailsPanelData(DataSource selectedDataSource) { + private void updateDetailsPanelData(DataSource selectedDataSource, Long unallocatedFilesSize, String osDetails, String usage) { clearTableValues(); if (selectedDataSource != null) { - String sizeString = ""; - String sectorSizeString = ""; - String md5String = ""; - String sha1String = ""; - String sha256String = ""; - String acquisitionDetailsString = ""; - String imageTypeString = ""; - String[] filePaths = new String[0]; - String osDetailString = osDetailMap.get(selectedDataSource.getId()) == null ? "" : osDetailMap.get(selectedDataSource.getId()); - String dataSourceTypeString = usageMap.get(selectedDataSource.getId()) == null ? "" : usageMap.get(selectedDataSource.getId()); - try { - acquisitionDetailsString = selectedDataSource.getAcquisitionDetails(); - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get aquisition details for selected data source", ex); - } - if (selectedDataSource instanceof Image) { - imageTypeString = ((Image) selectedDataSource).getType().getName(); - filePaths = ((Image) selectedDataSource).getPaths(); - sizeString = getSizeString(selectedDataSource.getSize()); - sectorSizeString = getSizeString(((Image) selectedDataSource).getSsize()); - try { - //older databases may have null as the hash values - md5String = ((Image) selectedDataSource).getMd5(); - if (md5String == null) { - md5String = ""; - } - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get MD5 for selected data source", ex); - } - try { - sha1String = ((Image) selectedDataSource).getSha1(); - if (sha1String == null) { - sha1String = ""; - } - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get SHA1 for selected data source", ex); - } - try { - sha256String = ((Image) selectedDataSource).getSha256(); - if (sha256String == null) { - sha256String = ""; - } - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get SHA256 for selected data source", ex); - } - } + unallocatedSizeValue.setText(getSizeString(unallocatedFilesSize)); + operatingSystemValue.setText(StringUtils.isBlank(osDetails) ? "" : osDetails); + dataSourceUsageValue.setText(StringUtils.isBlank(usage) ? "" : usage); + + timeZoneValue.setText(selectedDataSource.getTimeZone()); displayNameValue.setText(selectedDataSource.getName()); originalNameValue.setText(selectedDataSource.getName()); deviceIdValue.setText(selectedDataSource.getDeviceId()); - dataSourceUsageValue.setText(dataSourceTypeString); - operatingSystemValue.setText(osDetailString); - timeZoneValue.setText(selectedDataSource.getTimeZone()); - acquisitionDetailsTextArea.setText(acquisitionDetailsString); - imageTypeValue.setText(imageTypeString); - sizeValue.setText(sizeString); - unallocatedSizeValue.setText(getSizeString(unallocatedFilesSizeMap.get(selectedDataSource.getId()))); - sectorSizeValue.setText(sectorSizeString); - md5HashValue.setText(md5String); - sha1HashValue.setText(sha1String); - sha256HashValue.setText(sha256String); - for (String path : filePaths) { - ((DefaultTableModel) filePathsTable.getModel()).addRow(new Object[]{path}); + + try { + acquisitionDetailsTextArea.setText(selectedDataSource.getAcquisitionDetails()); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Unable to get aquisition details for selected data source", ex); + } + + if (selectedDataSource instanceof Image) { + setFieldsForImage((Image) selectedDataSource); } } updateFieldVisibility(); this.repaint(); } + /** + * Sets text fields for an image. This should be called after + * clearTableValues and before updateFieldVisibility to ensure the proper + * rendering. + * + * @param selectedImage The selected image. + */ + private void setFieldsForImage(Image selectedImage) { + imageTypeValue.setText(selectedImage.getType().getName()); + sizeValue.setText(getSizeString(selectedImage.getSize())); + sectorSizeValue.setText(getSizeString(selectedImage.getSsize())); + + for (String path : selectedImage.getPaths()) { + ((DefaultTableModel) filePathsTable.getModel()).addRow(new Object[]{path}); + } + + try { + //older databases may have null as the hash values + String md5String = selectedImage.getMd5(); + if (md5String == null) { + md5String = ""; + } + md5HashValue.setText(md5String); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Unable to get MD5 for selected data source", ex); + } + + try { + String sha1String = selectedImage.getSha1(); + if (sha1String == null) { + sha1String = ""; + } + sha1HashValue.setText(sha1String); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Unable to get SHA1 for selected data source", ex); + } + + try { + String sha256String = selectedImage.getSha256(); + if (sha256String == null) { + sha256String = ""; + } + sha256HashValue.setText(sha256String); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Unable to get SHA256 for selected data source", ex); + } + } + /** * Get a long size in bytes as a string formated to be read by users. * @@ -147,7 +174,7 @@ class DataSourceSummaryDetailsPanel extends javax.swing.JPanel { "DataSourceSummaryDetailsPanel.units.terabytes= TB", "DataSourceSummaryDetailsPanel.units.petabytes= PB" }) - private String getSizeString(Long size) { + private static String getSizeString(Long size) { if (size == null) { return ""; } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.form b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.form index 72c45cb5a7..272fc9b041 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.form +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.form @@ -68,6 +68,11 @@ + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java index 3c68b7b3ed..e1197fa83a 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java @@ -27,7 +27,6 @@ import java.util.Observer; import java.util.Set; import javax.swing.event.ListSelectionEvent; import org.openide.util.NbBundle.Messages; -import org.sleuthkit.autopsy.casemodule.IngestJobInfoPanel; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent; import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent.Reason; @@ -41,10 +40,8 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser private static final long serialVersionUID = 1L; private static final Set INGEST_JOB_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestJobEvent.DATA_SOURCE_ANALYSIS_COMPLETED); - private final DataSourceSummaryCountsPanel countsPanel; - private final DataSourceSummaryDetailsPanel detailsPanel; private final DataSourceBrowser dataSourcesPanel; - private final IngestJobInfoPanel ingestHistoryPanel; + private final DataSourceSummaryTabbedPane dataSourceSummaryTabbedPane; /** * Creates new form DataSourceSummaryDialog for displaying a summary of the @@ -61,21 +58,14 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser super(owner, Bundle.DataSourceSummaryDialog_window_title(), true); Map usageMap = DataSourceInfoUtilities.getDataSourceTypes(); Map fileCountsMap = DataSourceInfoUtilities.getCountsOfFiles(); - countsPanel = new DataSourceSummaryCountsPanel(fileCountsMap); - detailsPanel = new DataSourceSummaryDetailsPanel(usageMap); dataSourcesPanel = new DataSourceBrowser(usageMap, fileCountsMap); - ingestHistoryPanel = new IngestJobInfoPanel(); + dataSourceSummaryTabbedPane = new DataSourceSummaryTabbedPane(); initComponents(); dataSourceSummarySplitPane.setLeftComponent(dataSourcesPanel); - dataSourceTabbedPane.addTab(Bundle.DataSourceSummaryDialog_detailsTab_title(), detailsPanel); - dataSourceTabbedPane.addTab(Bundle.DataSourceSummaryDialog_countsTab_title(), countsPanel); - dataSourceTabbedPane.addTab(Bundle.DataSourceSummaryDialog_ingestHistoryTab_title(), ingestHistoryPanel); dataSourcesPanel.addListSelectionListener((ListSelectionEvent e) -> { if (!e.getValueIsAdjusting()) { DataSource selectedDataSource = dataSourcesPanel.getSelectedDataSource(); - countsPanel.updateCountsTableData(selectedDataSource); - detailsPanel.updateDetailsPanelData(selectedDataSource); - ingestHistoryPanel.setDataSource(selectedDataSource); + dataSourceSummaryTabbedPane.setDataSource(selectedDataSource); this.repaint(); } }); @@ -116,7 +106,7 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser closeButton = new javax.swing.JButton(); dataSourceSummarySplitPane = new javax.swing.JSplitPane(); - dataSourceTabbedPane = new javax.swing.JTabbedPane(); + javax.swing.JTabbedPane dataSourceTabbedPane = dataSourceSummaryTabbedPane; org.openide.awt.Mnemonics.setLocalizedText(closeButton, org.openide.util.NbBundle.getMessage(DataSourceSummaryDialog.class, "DataSourceSummaryDialog.closeButton.text")); // NOI18N closeButton.addActionListener(new java.awt.event.ActionListener() { @@ -173,6 +163,5 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton closeButton; private javax.swing.JSplitPane dataSourceSummarySplitPane; - private javax.swing.JTabbedPane dataSourceTabbedPane; // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryTabbedPane.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryTabbedPane.java new file mode 100644 index 0000000000..e1d15a6b0d --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryTabbedPane.java @@ -0,0 +1,74 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit 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.casemodule.datasourcesummary; + +import javax.swing.JTabbedPane; +import org.sleuthkit.autopsy.casemodule.IngestJobInfoPanel; +import org.sleuthkit.datamodel.DataSource; + +/** + * A tabbed pane showing the summary of a data source including tabs of: + * DataSourceSummaryCountsPanel, DataSourceSummaryDetailsPanel, and + * IngestJobInfoPanel. + */ +public class DataSourceSummaryTabbedPane extends JTabbedPane { + + private static final long serialVersionUID = 1L; + + private final DataSourceSummaryCountsPanel countsPanel; + private final DataSourceSummaryDetailsPanel detailsPanel; + private final IngestJobInfoPanel ingestHistoryPanel; + + private DataSource dataSource = null; + + /** + * Constructs a tabbed pane showing the summary of a data source. + */ + public DataSourceSummaryTabbedPane() { + countsPanel = new DataSourceSummaryCountsPanel(); + detailsPanel = new DataSourceSummaryDetailsPanel(); + ingestHistoryPanel = new IngestJobInfoPanel(); + + addTab(Bundle.DataSourceSummaryDialog_detailsTab_title(), detailsPanel); + addTab(Bundle.DataSourceSummaryDialog_countsTab_title(), countsPanel); + addTab(Bundle.DataSourceSummaryDialog_ingestHistoryTab_title(), ingestHistoryPanel); + } + + /** + * The datasource currently used as the model in this panel. + * + * @return The datasource currently being used as the model in this panel. + */ + public DataSource getDataSource() { + return dataSource; + } + + /** + * Sets datasource to visualize in the tabbed panel. + * + * @param dataSource The datasource to use in this panel. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + + detailsPanel.setDataSource(dataSource); + countsPanel.setDataSource(dataSource); + ingestHistoryPanel.setDataSource(dataSource); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/NonEditableTableModel.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/NonEditableTableModel.java new file mode 100644 index 0000000000..8ed965e2ab --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/NonEditableTableModel.java @@ -0,0 +1,37 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit 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.casemodule.datasourcesummary; + +import javax.swing.table.DefaultTableModel; + +/** + * A Table model where cells are not editable. + */ +class NonEditableTableModel extends DefaultTableModel { + private static final long serialVersionUID = 1L; + + NonEditableTableModel(Object[][] data, Object[] columnNames) { + super(data, columnNames); + } + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/services/TagsManager.java b/Core/src/org/sleuthkit/autopsy/casemodule/services/TagsManager.java index 746bac478c..1a9b2e792d 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/services/TagsManager.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/services/TagsManager.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2020 Basis Technology Corp. + * Copyright 2013-2020 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -56,7 +56,7 @@ public class TagsManager implements Closeable { private final SleuthkitCase caseDb; private static String DEFAULT_TAG_SET_NAME = "Project VIC"; - + private static final Object lock = new Object(); static { @@ -235,16 +235,16 @@ public class TagsManager implements Closeable { public static String getNotableTagDisplayName() { return TagNameDefinition.getNotableTagDisplayName(); } - + /** * Creates a new TagSetDefinition file. - * + * * @param tagSetDef The tag set definition. - * - * @throws IOException + * + * @throws IOException */ public static void addTagSetDefinition(TagSetDefinition tagSetDef) throws IOException { - synchronized(lock) { + synchronized (lock) { TagSetDefinition.writeTagSetDefinition(tagSetDef); } } @@ -267,20 +267,20 @@ public class TagsManager implements Closeable { caseDb.addOrUpdateTagName(def.getDisplayName(), def.getDescription(), def.getColor(), def.getKnownStatus()); } //Assume new case and add tag sets - for(TagSetDefinition setDef: TagSetDefinition.readTagSetDefinitions()) { + for (TagSetDefinition setDef : TagSetDefinition.readTagSetDefinitions()) { List tagNameList = new ArrayList<>(); - for(TagNameDefinition tagNameDef: setDef.getTagNameDefinitions()) { + for (TagNameDefinition tagNameDef : setDef.getTagNameDefinitions()) { tagNameList.add(caseDb.addOrUpdateTagName(tagNameDef.getDisplayName(), tagNameDef.getDescription(), tagNameDef.getColor(), tagNameDef.getKnownStatus())); } - - if(!tagNameList.isEmpty()) { + + if (!tagNameList.isEmpty()) { taggingMgr.addTagSet(setDef.getName(), tagNameList); } } } } catch (TskCoreException ex) { LOGGER.log(Level.SEVERE, "Error updating standard tag name and tag set definitions", ex); - } catch(IOException ex) { + } catch (IOException ex) { LOGGER.log(Level.SEVERE, "Error loading tag set JSON files", ex); } @@ -288,28 +288,41 @@ public class TagsManager implements Closeable { tagName.saveToCase(caseDb); } } - + /** * Get a list of all tag sets currently in the case database. - * + * * @return A list, possibly empty, of TagSet objects. - * + * * @throws TskCoreException */ public List getAllTagSets() throws TskCoreException { return caseDb.getTaggingManager().getTagSets(); } - + + /** + * Gets the tag set a tag name (tag definition) belongs to, if any. + * + * @param tagName The tag name. + * + * @return A TagSet object or null. + * + * @throws TskCoreException If there is an error querying the case database. + */ + public TagSet getTagSet(TagName tagName) throws TskCoreException { + return caseDb.getTaggingManager().getTagSet(tagName); + } + /** * Add a new TagSet to the case database. Tags will be ranked in the order * which they are passed to this method. - * - * @param name Tag set name. + * + * @param name Tag set name. * @param tagNameList List of TagName in rank order. - * + * * @return A new TagSet object. - * - * @throws TskCoreException + * + * @throws TskCoreException */ public TagSet addTagSet(String name, List tagNameList) throws TskCoreException { return caseDb.getTaggingManager().addTagSet(name, tagNameList); @@ -501,7 +514,7 @@ public class TagsManager implements Closeable { * name to the case database. */ public TagName addTagName(String displayName, String description, TagName.HTML_COLOR color, TskData.FileKnown knownStatus) throws TagNameAlreadyExistsException, TskCoreException { - synchronized(lock) { + synchronized (lock) { try { TagName tagName = caseDb.addOrUpdateTagName(displayName, description, color, knownStatus); Set customTypes = TagNameDefinition.getTagNameDefinitions(); diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/Bundle.properties b/Core/src/org/sleuthkit/autopsy/centralrepository/Bundle.properties index 1c7e6c2d7e..37d4c54c72 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/Bundle.properties @@ -1,9 +1,9 @@ OpenIDE-Module-Name=Central Repository OpenIDE-Module-Display-Category=Ingest Module -OpenIDE-Module-Short-Description=Correlation Engine Ingest Module +OpenIDE-Module-Short-Description=Central Repository Ingest Module OpenIDE-Module-Long-Description=\ - Correlation Engine ingest module and central database. \n\n\ - The Correlation Engine ingest module stores attributes of artifacts matching selected correlation types into a central database.\n\ + Central Repository ingest module and central database. \n\n\ + The Central Repository ingest module stores attributes of artifacts matching selected correlation types into a central database.\n\ Stored attributes are used in future cases to correlate and analyzes files and artifacts during ingest. CentralRepoCommentDialog.commentLabel.text=Comment: CentralRepoCommentDialog.okButton.text=&OK diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/centralrepository/Bundle.properties-MERGED index 007af703c5..b4f7f835ef 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/Bundle.properties-MERGED @@ -4,10 +4,10 @@ AddEditCentralRepoCommentAction.menuItemText.addEditCentralRepoCommentNoMD5=Add/ CentralRepoCommentDialog.title.addEditCentralRepoComment=Add/Edit Central Repository Comment OpenIDE-Module-Name=Central Repository OpenIDE-Module-Display-Category=Ingest Module -OpenIDE-Module-Short-Description=Correlation Engine Ingest Module +OpenIDE-Module-Short-Description=Central Repository Ingest Module OpenIDE-Module-Long-Description=\ - Correlation Engine ingest module and central database. \n\n\ - The Correlation Engine ingest module stores attributes of artifacts matching selected correlation types into a central database.\n\ + Central Repository ingest module and central database. \n\n\ + The Central Repository ingest module stores attributes of artifacts matching selected correlation types into a central database.\n\ Stored attributes are used in future cases to correlate and analyzes files and artifacts during ingest. CentralRepoCommentDialog.commentLabel.text=Comment: CentralRepoCommentDialog.okButton.text=&OK diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoAccount.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoAccount.java index 8c92cb1f6c..88c74a4fe8 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoAccount.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoAccount.java @@ -23,25 +23,28 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.apache.commons.lang.StringUtils; import org.sleuthkit.datamodel.Account; - +import org.sleuthkit.datamodel.InvalidAccountIDException; /** * This class abstracts an Account as stored in the CR database. */ public final class CentralRepoAccount { - - // primary key in the Accounts table in CR database - private final long accountId; - private final CentralRepoAccountType accountType; - - // type specific unique account identifier - private final String typeSpecificIdentifier; - + // primary key in the Accounts table in CR database + private final long accountId; + + private final CentralRepoAccountType accountType; + + // type specific unique account identifier + private final String typeSpecificIdentifier; + /** - * Encapsulates a central repo account type and the correlation type - * that it maps to. + * Encapsulates a central repo account type and the correlation type that it + * maps to. */ public static final class CentralRepoAccountType { @@ -55,7 +58,6 @@ public final class CentralRepoAccount { this.correlationTypeId = correlationTypeId; this.accountTypeId = acctTypeID; } - /** * @return the acctType @@ -71,72 +73,103 @@ public final class CentralRepoAccount { public int getAccountTypeId() { return this.accountTypeId; } + + @Override + public int hashCode() { + int hash = 5; + hash = 29 * hash + this.accountTypeId; + hash = 29 * hash + Objects.hashCode(this.acctType); + hash = 29 * hash + this.correlationTypeId; + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final CentralRepoAccountType other = (CentralRepoAccountType) obj; + if (this.accountTypeId != other.getAccountTypeId()) { + return false; + } + if (this.correlationTypeId != other.getCorrelationTypeId()) { + return false; + } + return Objects.equals(this.acctType, other.getAcctType()); + } + } - + public CentralRepoAccount(long accountId, CentralRepoAccountType accountType, String typeSpecificIdentifier) { - this.accountId = accountId; - this.accountType = accountType; - this.typeSpecificIdentifier = typeSpecificIdentifier; - } + this.accountId = accountId; + this.accountType = accountType; + this.typeSpecificIdentifier = typeSpecificIdentifier; + } - /** - * Gets unique identifier (assigned by a provider) for the account. Example - * includes an email address, a phone number, or a website username. - * - * @return type specific account id. - */ - public String getIdentifier() { - return this.typeSpecificIdentifier; - } + /** + * Gets unique identifier (assigned by a provider) for the account. Example + * includes an email address, a phone number, or a website username. + * + * @return type specific account id. + */ + public String getIdentifier() { + return this.typeSpecificIdentifier; + } - /** - * Gets the account type - * - * @return account type - */ - public CentralRepoAccountType getAccountType() { - return this.accountType; - } + /** + * Gets the account type + * + * @return account type + */ + public CentralRepoAccountType getAccountType() { + return this.accountType; + } - /** - * Gets the unique row id for this account in the database. - * - * @return unique row id. - */ - public long getId() { - return this.accountId; - } + /** + * Gets the unique row id for this account in the database. + * + * @return unique row id. + */ + public long getId() { + return this.accountId; + } - @Override - public int hashCode() { - int hash = 5; - hash = 43 * hash + (int) (this.accountId ^ (this.accountId >>> 32)); - hash = 43 * hash + (this.accountType != null ? this.accountType.hashCode() : 0); - hash = 43 * hash + (this.typeSpecificIdentifier != null ? this.typeSpecificIdentifier.hashCode() : 0); - return hash; - } + @Override + public int hashCode() { + int hash = 5; + hash = 43 * hash + (int) (this.accountId ^ (this.accountId >>> 32)); + hash = 43 * hash + (this.accountType != null ? this.accountType.hashCode() : 0); + hash = 43 * hash + (this.typeSpecificIdentifier != null ? this.typeSpecificIdentifier.hashCode() : 0); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final CentralRepoAccount other = (CentralRepoAccount) obj; + if (this.accountId != other.getId()) { + return false; + } + if (!Objects.equals(this.typeSpecificIdentifier, other.getIdentifier())) { + return false; + } + return Objects.equals(this.accountType, other.getAccountType()); + } - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - final CentralRepoAccount other = (CentralRepoAccount) obj; - if (this.accountId != other.getId()) { - return false; - } - if ((this.typeSpecificIdentifier == null) ? (other.getIdentifier() != null) : !this.typeSpecificIdentifier.equals(other.getIdentifier())) { - return false; - } - return !(this.accountType != other.getAccountType() && (this.accountType == null || !this.accountType.equals(other.getAccountType()))); - } - /** * Callback to process a query that gets accounts */ @@ -152,7 +185,7 @@ public final class CentralRepoAccount { // create account Account.Type acctType = new Account.Type(rs.getString("type_name"), rs.getString("display_name")); CentralRepoAccountType crAccountType = new CentralRepoAccountType(rs.getInt("account_type_id"), acctType, rs.getInt("correlation_type_id")); - + CentralRepoAccount account = new CentralRepoAccount( rs.getInt("account_id"), crAccountType, @@ -167,7 +200,6 @@ public final class CentralRepoAccount { } }; - private static final String ACCOUNTS_QUERY_CLAUSE = "SELECT accounts.id as account_id, " + " accounts.account_type_id as account_type_id, accounts.account_unique_identifier as account_unique_identifier," @@ -175,66 +207,138 @@ public final class CentralRepoAccount { + " account_types.type_name as type_name, account_types.display_name as display_name, account_types.correlation_type_id as correlation_type_id " + " FROM accounts " + " JOIN account_types as account_types on accounts.account_type_id = account_types.id "; - - - /** + + /** * Get all accounts with account identifier matching the given substring. * - * @param accountIdentifierSubstring Account identifier substring to look for. + * @param accountIdentifierSubstring Account identifier substring to look + * for. * - * @return Collection of all accounts with identifier matching the given substring, may - * be empty. - * - * @throws CentralRepoException If there is an error in getting the accounts. + * @return Collection of all accounts with identifier matching the given + * substring, may be empty. + * + * @throws CentralRepoException If there is an error in getting the + * accounts. */ public static Collection getAccountsWithIdentifierLike(String accountIdentifierSubstring) throws CentralRepoException { - + String queryClause = ACCOUNTS_QUERY_CLAUSE - + " WHERE LOWER(accounts.account_unique_identifier) LIKE LOWER('%" + accountIdentifierSubstring + "%')"; + + " WHERE LOWER(accounts.account_unique_identifier) LIKE LOWER(?)"; + + List params = new ArrayList<>(); + params.add("%" + accountIdentifierSubstring + "%"); AccountsQueryCallback queryCallback = new AccountsQueryCallback(); - CentralRepository.getInstance().executeSelectSQL(queryClause, queryCallback); + CentralRepository.getInstance().executeQuery(queryClause, params, queryCallback); return queryCallback.getAccountsList(); } - + /** * Get all accounts with account identifier matching the given identifier. * * @param accountIdentifier Account identifier to look for. * - * @return Collection of all accounts with identifier matching the given identifier, may - * be empty. - * - * @throws CentralRepoException If there is an error in getting the accounts. + * @return Collection of all accounts with identifier matching the given + * identifier, may be empty. + * + * @throws CentralRepoException If there is an error in getting the + * accounts. */ - public static Collection getAccountsWithIdentifier(String accountIdentifier) throws CentralRepoException { - + public static Collection getAccountsWithIdentifier(String accountIdentifier) throws InvalidAccountIDException, CentralRepoException { + + String normalizedAccountIdentifier = normalizeAccountIdentifier(accountIdentifier); String queryClause = ACCOUNTS_QUERY_CLAUSE - + " WHERE LOWER(accounts.account_unique_identifier) = LOWER('" + accountIdentifier + "')"; + + " WHERE LOWER(accounts.account_unique_identifier) = LOWER(?)"; + + List params = new ArrayList<>(); + params.add(normalizedAccountIdentifier); AccountsQueryCallback queryCallback = new AccountsQueryCallback(); - CentralRepository.getInstance().executeSelectSQL(queryClause, queryCallback); + CentralRepository.getInstance().executeQuery(queryClause, params, queryCallback); return queryCallback.getAccountsList(); } - + /** * Get all central repo accounts. * - * @return Collection of all accounts with identifier matching the given identifier, may - * be empty. - * - * @throws CentralRepoException If there is an error in getting the accounts. + * @return Collection of all accounts with identifier matching the given + * identifier, may be empty. + * + * @throws CentralRepoException If there is an error in getting the + * accounts. */ public static Collection getAllAccounts() throws CentralRepoException { - + String queryClause = ACCOUNTS_QUERY_CLAUSE; + List params = new ArrayList<>(); // empty param list + AccountsQueryCallback queryCallback = new AccountsQueryCallback(); - CentralRepository.getInstance().executeSelectSQL(queryClause, queryCallback); + CentralRepository.getInstance().executeQuery(queryClause, params, queryCallback); return queryCallback.getAccountsList(); } + + /** + * Attempts to normalize an account identifier, after trying to guess the + * account type. + * + * @param accountIdentifier Account identifier to be normalized. + * @return normalized identifier + * + * @throws InvalidAccountIDException If the account identifier is not valid. + */ + private static String normalizeAccountIdentifier(String accountIdentifier) throws InvalidAccountIDException { + if (StringUtils.isEmpty(accountIdentifier)) { + throw new InvalidAccountIDException("Account id is null or empty."); + } + + String normalizedAccountIdentifier; + try { + if (CorrelationAttributeNormalizer.isValidPhoneNumber(accountIdentifier)) { + normalizedAccountIdentifier = CorrelationAttributeNormalizer.normalizePhone(accountIdentifier); + } else if (CorrelationAttributeNormalizer.isValidEmailAddress(accountIdentifier)) { + normalizedAccountIdentifier = CorrelationAttributeNormalizer.normalizeEmail(accountIdentifier); + } else { + normalizedAccountIdentifier = accountIdentifier.toLowerCase().trim(); + } + } catch (CorrelationAttributeNormalizationException ex) { + throw new InvalidAccountIDException("Failed to normalize the account idenitier " + accountIdentifier, ex); + } + return normalizedAccountIdentifier; + } + /** + * Normalizes an account identifier, based on the given account type. + * + * @param crAccountType Account type. + * @param accountIdentifier Account identifier to be normalized. + * @return Normalized identifier. + * + * @throws InvalidAccountIDException If the account identifier is invalid. + */ + public static String normalizeAccountIdentifier(CentralRepoAccountType crAccountType, String accountIdentifier) throws InvalidAccountIDException { + + if (StringUtils.isBlank(accountIdentifier)) { + throw new InvalidAccountIDException("Account identifier is null or empty."); + } + + String normalizedAccountIdentifier; + try { + if (crAccountType.getAcctType().equals(Account.Type.PHONE)) { + normalizedAccountIdentifier = CorrelationAttributeNormalizer.normalizePhone(accountIdentifier); + } else if (crAccountType.getAcctType().equals(Account.Type.EMAIL)) { + normalizedAccountIdentifier = CorrelationAttributeNormalizer.normalizeEmail(accountIdentifier); + } else { + // convert to lowercase + normalizedAccountIdentifier = accountIdentifier.toLowerCase(); + } + } catch (CorrelationAttributeNormalizationException ex) { + throw new InvalidAccountIDException("Invalid account identifier", ex); + } + + return normalizedAccountIdentifier; + } } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbUtil.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbUtil.java index 69ea8d4e05..5105aed2e9 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbUtil.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbUtil.java @@ -25,6 +25,9 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.List; import java.util.logging.Level; +import javax.swing.SwingUtilities; +import org.openide.windows.TopComponent; +import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ModuleSettings; import static org.sleuthkit.autopsy.centralrepository.datamodel.RdbmsCentralRepo.SOFTWARE_CR_DB_SCHEMA_VERSION; @@ -259,8 +262,21 @@ public class CentralRepoDbUtil { * used */ public static void setUseCentralRepo(boolean centralRepoCheckBoxIsSelected) { + closePersonasTopComponent(); ModuleSettings.setConfigSetting(CENTRAL_REPO_NAME, CENTRAL_REPO_USE_KEY, Boolean.toString(centralRepoCheckBoxIsSelected)); } + + /** + * Closes Personas top component if it exists. + */ + private static void closePersonasTopComponent() { + SwingUtilities.invokeLater(() -> { + TopComponent personasWindow = WindowManager.getDefault().findTopComponent("PersonasTopComponent"); + if (personasWindow != null && personasWindow.isOpened()) { + personasWindow.close(); + } + }); + } /** * Use the current settings and the validation query to test the connection diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepository.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepository.java index a6af74ad3c..842c8e3f04 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepository.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepository.java @@ -27,6 +27,7 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoAccount.CentralRepoAccountType; import org.sleuthkit.autopsy.coordinationservice.CoordinationService; import org.sleuthkit.datamodel.HashHitInfo; +import org.sleuthkit.datamodel.InvalidAccountIDException; /** * Main interface for interacting with the database @@ -64,7 +65,8 @@ public interface CentralRepository { * It will not close active/in-use connections. Thus, it is vital that there * are no in-use connections when you call this method. * - * @throws CentralRepoException if there is a problem closing the connection pool. + * @throws CentralRepoException if there is a problem closing the connection + * pool. */ void shutdownConnections() throws CentralRepoException; @@ -101,7 +103,7 @@ public interface CentralRepository { /** * Add a new name/value pair in the db_info table. * - * @param name Key to set + * @param name Key to set * @param value Value to set * * @throws CentralRepoException @@ -112,9 +114,9 @@ public interface CentralRepository { * Set the data source object id for a specific entry in the data_sources * table * - * @param rowId - the row id for the data_sources table entry + * @param rowId - the row id for the data_sources table entry * @param dataSourceObjectId - the object id for the data source from the - * caseDb + * caseDb */ void addDataSourceObjectId(int rowId, long dataSourceObjectId) throws CentralRepoException; @@ -132,7 +134,7 @@ public interface CentralRepository { /** * Update the value for a name in the name/value db_info table. * - * @param name Name to find + * @param name Name to find * @param value Value to assign to name. * * @throws CentralRepoException @@ -161,10 +163,10 @@ public interface CentralRepository { * @param eamCase The case to update */ void updateCase(CorrelationCase eamCase) throws CentralRepoException; - + /** - * Queries the examiner table for the given user name. - * Adds a row if the user is not found in the examiner table. + * Queries the examiner table for the given user name. Adds a row if the + * user is not found in the examiner table. * * @param examinerLoginName user name to look for. * @return CentralRepoExaminer for the given user name. @@ -215,7 +217,7 @@ public interface CentralRepository { * @param eamDataSource the data source to add * * @return - A CorrelationDataSource object with data source's central - * repository id + * repository id */ CorrelationDataSource newDataSource(CorrelationDataSource eamDataSource) throws CentralRepoException; @@ -244,8 +246,8 @@ public interface CentralRepository { /** * Retrieves Data Source details based on data source device ID * - * @param correlationCase the current CorrelationCase used for ensuring - * uniqueness of DataSource + * @param correlationCase the current CorrelationCase used for ensuring + * uniqueness of DataSource * @param caseDbDataSourceId the data source device ID number * * @return The data source @@ -256,8 +258,8 @@ public interface CentralRepository { * Retrieves Data Source details based on data source ID * * @param correlationCase the current CorrelationCase used for ensuring - * uniqueness of DataSource - * @param dataSourceId the data source ID number + * uniqueness of DataSource + * @param dataSourceId the data source ID number * * @return The data source */ @@ -274,7 +276,7 @@ public interface CentralRepository { * Changes the name of a data source in the DB * * @param eamDataSource The data source - * @param newName The new name + * @param newName The new name * * @throws CentralRepoException */ @@ -292,12 +294,12 @@ public interface CentralRepository { * Retrieves eamArtifact instances from the database that are associated * with the eamArtifactType and eamArtifactValues of the given eamArtifact. * - * @param aType EamArtifact.Type to search for + * @param aType EamArtifact.Type to search for * @param values The list of correlation values to get - * CorrelationAttributeInstances for + * CorrelationAttributeInstances for * * @return List of artifact instances for a given type with the specified - * values + * values * * @throws CorrelationAttributeNormalizationException * @throws CentralRepoException @@ -323,14 +325,14 @@ public interface CentralRepository { * with the eamArtifactType and eamArtifactValues of the given eamArtifact * for the specified cases. * - * @param aType The type of the artifact - * @param values The list of correlation values to get - * CorrelationAttributeInstances for + * @param aType The type of the artifact + * @param values The list of correlation values to get + * CorrelationAttributeInstances for * @param caseIds The list of central repository case ids to get - * CorrelationAttributeInstances for + * CorrelationAttributeInstances for * * @return List of artifact instances for a given type with the specified - * values for the specified cases + * values for the specified cases * * @throws CorrelationAttributeNormalizationException * @throws CentralRepoException @@ -345,7 +347,7 @@ public interface CentralRepository { * @param value Value to search for * * @return Number of artifact instances having ArtifactType and - * ArtifactValue. + * ArtifactValue. */ Long getCountArtifactInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String value) throws CentralRepoException, CorrelationAttributeNormalizationException; @@ -384,7 +386,7 @@ public interface CentralRepository { * @param correlationDataSource Data source to search for * * @return Number of artifact instances having caseDisplayName and - * dataSource + * dataSource */ Long getCountArtifactInstancesByCaseDataSource(CorrelationDataSource correlationDataSource) throws CentralRepoException; @@ -413,7 +415,7 @@ public interface CentralRepository { * in the associated CorrelationAttribute object. * * @param eamArtifact The correlation attribute whose database instance will - * be updated. + * be updated. * * @throws CentralRepoException */ @@ -426,11 +428,11 @@ public interface CentralRepository { * Method exists to support instances added using Central Repository version * 1,1 and older * - * @param type The type of instance. - * @param correlationCase The case tied to the instance. + * @param type The type of instance. + * @param correlationCase The case tied to the instance. * @param correlationDataSource The data source tied to the instance. - * @param value The value tied to the instance. - * @param filePath The file path tied to the instance. + * @param value The value tied to the instance. + * @param filePath The file path tied to the instance. * * @return The correlation attribute if it exists; otherwise null. * @@ -443,11 +445,10 @@ public interface CentralRepository { * Find a correlation attribute in the Central Repository database given the * instance type, case, data source, object id. * - * @param type The type of instance. - * @param correlationCase The case tied to the instance. + * @param type The type of instance. + * @param correlationCase The case tied to the instance. * @param correlationDataSource The data source tied to the instance. - * @param objectID The object id of the file tied to the - * instance. + * @param objectID The object id of the file tied to the instance. * * @return The correlation attribute if it exists; otherwise null. * @@ -483,7 +484,7 @@ public interface CentralRepository { * @param value Value to search for * * @return List of cases containing this artifact with instances marked as - * bad + * bad * * @throws CentralRepoException */ @@ -497,7 +498,7 @@ public interface CentralRepository { * @param value Value to search for * * @return List of cases containing this artifact with instances marked as - * bad + * bad * * @throws CentralRepoException */ @@ -554,13 +555,13 @@ public interface CentralRepository { */ public boolean isFileHashInReferenceSet(String hash, int referenceSetID) throws CentralRepoException, CorrelationAttributeNormalizationException; - /** - * Retrieves the given file HashHitInfo if the given file hash is in this + * Retrieves the given file HashHitInfo if the given file hash is in this * reference set. Only searches the reference_files table. * * @param hash The hash to find in a search. - * @param referenceSetID The referenceSetID within which the file should exist. + * @param referenceSetID The referenceSetID within which the file should + * exist. * * @return The HashHitInfo if found or null if not found. * @@ -569,7 +570,6 @@ public interface CentralRepository { */ HashHitInfo lookupHash(String hash, int referenceSetID) throws CentralRepoException, CorrelationAttributeNormalizationException; - /** * Check if the given value is in a specific reference set * @@ -637,7 +637,7 @@ public interface CentralRepository { * Update an existing organization. * * @param updatedOrganization the values the Organization with the same ID - * will be updated to in the database. + * will be updated to in the database. * * @throws CentralRepoException */ @@ -689,8 +689,7 @@ public interface CentralRepository { * Add a new reference instance * * @param eamGlobalFileInstance The reference instance to add - * @param correlationType Correlation Type that this Reference - * Instance is + * @param correlationType Correlation Type that this Reference Instance is * * @throws CentralRepoException */ @@ -700,8 +699,8 @@ public interface CentralRepository { * Insert the bulk collection of Global File Instances * * @param globalInstances a Set of EamGlobalFileInstances to insert into the - * db. - * @param contentType the Type of the global instances + * db. + * @param contentType the Type of the global instances * * @throws CentralRepoException */ @@ -710,7 +709,7 @@ public interface CentralRepository { /** * Get all reference entries having a given correlation type and value * - * @param aType Type to use for matching + * @param aType Type to use for matching * @param aValue Value to use for matching * * @return List of all global file instances with a type and value @@ -735,7 +734,7 @@ public interface CentralRepository { * used to correlate artifacts. * * @return List of EamArtifact.Type's. If none are defined in the database, - * the default list will be returned. + * the default list will be returned. * * @throws CentralRepoException */ @@ -746,7 +745,7 @@ public interface CentralRepository { * artifacts. * * @return List of enabled EamArtifact.Type's. If none are defined in the - * database, the default list will be returned. + * database, the default list will be returned. * * @throws CentralRepoException */ @@ -757,7 +756,7 @@ public interface CentralRepository { * correlate artifacts. * * @return List of supported EamArtifact.Type's. If none are defined in the - * database, the default list will be returned. + * database, the default list will be returned. * * @throws CentralRepoException */ @@ -798,15 +797,15 @@ public interface CentralRepository { * * @return the lock, or null if locking is not supported * - * @throws CentralRepoException if the coordination service is running but we fail - * to get the lock + * @throws CentralRepoException if the coordination service is running but + * we fail to get the lock */ public CoordinationService.Lock getExclusiveMultiUserDbLock() throws CentralRepoException; /** * Process the Artifact instance in the EamDb * - * @param type EamArtifact.Type to search for + * @param type EamArtifact.Type to search for * @param instanceTableCallback callback to process the instance * * @throws CentralRepoException @@ -816,9 +815,9 @@ public interface CentralRepository { /** * Process the Artifact instance in the EamDb * - * @param type EamArtifact.Type to search for + * @param type EamArtifact.Type to search for * @param instanceTableCallback callback to process the instance - * @param whereClause query string to execute + * @param whereClause query string to execute * * @throws CentralRepoException */ @@ -827,78 +826,79 @@ public interface CentralRepository { /** * Process a SELECT query * - * @param selectClause query string to execute + * @param selectClause query string to execute * @param instanceTableCallback callback to process the instance * * @throws CentralRepoException */ - public void processSelectClause(String selectClause, InstanceTableCallback instanceTableCallback) throws CentralRepoException; - - + public void processSelectClause(String selectClause, InstanceTableCallback instanceTableCallback) throws CentralRepoException; + /** - * Executes an INSERT sql statement on the central repository database. - * @param sql INSERT sql to execute. - * - * @throws CentralRepoException If there is an error. + * Executes an INSERT/UPDATE/DELETE sql as a prepared statement, on the + * central repository database. + * + * @param sql sql to execute. + * @param params List of query params to use, may be empty. + * + * @throws CentralRepoException If there is an error. */ - void executeInsertSQL(String sql) throws CentralRepoException; - + void executeCommand(String sql, List params) throws CentralRepoException; + /** - * Executes a SELECT sql statement on the central repository database. - * - * @param sql SELECT sql to execute. + * Executes a SELECT query sql as a prepared statement, on the central + * repository database. + * + * @param sql sql to execute. + * @param params List of query params to use, may be empty. * @param queryCallback Query callback to handle the result of the query. - * + * * @throws CentralRepoException If there is an error. */ - void executeSelectSQL(String sql, CentralRepositoryDbQueryCallback queryCallback) throws CentralRepoException; - - /** - * Executes an UPDATE sql statement on the central repository database. - * - * @param sql UPDATE sql to execute. - * - * @throws CentralRepoException If there is an error. - */ - void executeUpdateSQL(String sql) throws CentralRepoException; - - /** - * Executes a DELETE sql statement on the central repository database. - * - * @param sql DELETE sql to execute. - * - * @throws CentralRepoException If there is an error. - */ - void executeDeleteSQL(String sql) throws CentralRepoException; - + void executeQuery(String sql, List params, CentralRepositoryDbQueryCallback queryCallback) throws CentralRepoException; + /** * Get account type by type name. - * + * * @param accountTypeName account type name to look for * @return CR account type - * @throws CentralRepoException + * @throws CentralRepoException */ CentralRepoAccountType getAccountTypeByName(String accountTypeName) throws CentralRepoException; - + /** * Gets all account types. - * + * * @return Collection of all CR account types in the database. - * - * @throws CentralRepoException + * + * @throws CentralRepoException */ Collection getAllAccountTypes() throws CentralRepoException; - + /** - * Get an account from the accounts table matching the given type/ID. + * Get an account from the accounts table matching the given type/ID. * Inserts a row if one doesn't exists. - * + * * @param crAccountType CR account type to look for or create * @param accountUniqueID type specific unique account id * @return CR account * - * @throws CentralRepoException + * @throws CentralRepoException If there is an error accessing Central Repository. + * @throws InvalidAccountIDException If the account identifier is not valid. */ - CentralRepoAccount getOrCreateAccount(CentralRepoAccount.CentralRepoAccountType crAccountType, String accountUniqueID) throws CentralRepoException; - + CentralRepoAccount getOrCreateAccount(CentralRepoAccount.CentralRepoAccountType crAccountType, String accountUniqueID) throws InvalidAccountIDException, CentralRepoException; + + /** + * Gets an account from the accounts table matching the given type/ID, if + * one exists. + * + * @param crAccountType CR account type to look for or create + * @param accountUniqueID type specific unique account id + * + * @return CR account, if found, null otherwise. + * + * @throws CentralRepoException If there is an error accessing Central Repository. + * @throws InvalidAccountIDException If the account identifier is not valid. + */ + CentralRepoAccount getAccount(CentralRepoAccount.CentralRepoAccountType crAccountType, String accountUniqueID) throws InvalidAccountIDException, CentralRepoException; + } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizer.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizer.java index d762e74945..51b9b80f84 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizer.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizer.java @@ -19,12 +19,14 @@ */ package org.sleuthkit.autopsy.centralrepository.datamodel; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; +import org.apache.commons.lang.StringUtils; import org.apache.commons.validator.routines.DomainValidator; import org.apache.commons.validator.routines.EmailValidator; -import org.sleuthkit.datamodel.CommunicationsUtils; -import org.sleuthkit.datamodel.TskCoreException; /** * Provides functions for normalizing data by attribute type before insertion or @@ -40,7 +42,7 @@ final public class CorrelationAttributeNormalizer { * data is a valid string of the format expected given the attributeType. * * @param attributeType correlation type of data - * @param data data to normalize + * @param data data to normalize * * @return normalized data */ @@ -94,7 +96,7 @@ final public class CorrelationAttributeNormalizer { } catch (CentralRepoException ex) { throw new CorrelationAttributeNormalizationException("Failed to get default correlation types.", ex); } - } + } } /** @@ -102,7 +104,7 @@ final public class CorrelationAttributeNormalizer { * is a valid string of the format expected given the attributeType. * * @param attributeTypeId correlation type of data - * @param data data to normalize + * @param data data to normalize * * @return normalized data */ @@ -155,25 +157,43 @@ final public class CorrelationAttributeNormalizer { /** * Verify and normalize email address. + * + * @param emailAddress Address to normalize. + * @return Normalized email address. + * @throws CorrelationAttributeNormalizationExceptions If the input is not a + * valid email address. + * */ - private static String normalizeEmail(String data) throws CorrelationAttributeNormalizationException { - try { - return CommunicationsUtils.normalizeEmailAddress(data); - } - catch(TskCoreException ex) { - throw new CorrelationAttributeNormalizationException(String.format("Data was expected to be a valid email address: %s", data), ex); - } + static String normalizeEmail(String emailAddress) throws CorrelationAttributeNormalizationException { + if (isValidEmailAddress(emailAddress)) { + return emailAddress.toLowerCase().trim(); + } else { + throw new CorrelationAttributeNormalizationException(String.format("Data was expected to be a valid email address: %s", emailAddress)); + } } /** * Verify and normalize phone number. + * + * @param phoneNumber Phone number to normalize. + * @return Normalized phone number. + * @throws CorrelationAttributeNormalizationExceptions If the input is not a + * valid phone number. + * */ - private static String normalizePhone(String data) throws CorrelationAttributeNormalizationException { - try { - return CommunicationsUtils.normalizePhoneNum(data); - } - catch(TskCoreException ex) { - throw new CorrelationAttributeNormalizationException(String.format("Data was expected to be a valid phone number: %s", data)); + static String normalizePhone(String phoneNumber) throws CorrelationAttributeNormalizationException { + if (isValidPhoneNumber(phoneNumber)) { + String normalizedNumber = phoneNumber.replaceAll("\\s+", ""); // remove spaces. + normalizedNumber = normalizedNumber.replaceAll("[\\-()]", ""); // remove parens & dashes. + + // ensure a min length + if (normalizedNumber.length() < MIN_PHONENUMBER_LEN) { + throw new CorrelationAttributeNormalizationException(String.format("Phone number string %s is too short ", phoneNumber)); + } + return normalizedNumber; + + } else { + throw new CorrelationAttributeNormalizationException(String.format("Data was expected to be a valid phone number: %s", phoneNumber)); } } @@ -196,7 +216,7 @@ final public class CorrelationAttributeNormalizer { * @return the unmodified data if the data was a valid length to be an SSID * * @throws CorrelationAttributeNormalizationException if the data was not a - * valid SSID + * valid SSID */ private static String verifySsid(String data) throws CorrelationAttributeNormalizationException { if (data.length() <= 32) { @@ -223,10 +243,10 @@ final public class CorrelationAttributeNormalizer { * @param data The string to normalize and validate * * @return the data with common number seperators removed and lower cased if - * the data was determined to be a possible ICCID + * the data was determined to be a possible ICCID * * @throws CorrelationAttributeNormalizationException if the data was not a - * valid ICCID + * valid ICCID */ private static String normalizeIccid(String data) throws CorrelationAttributeNormalizationException { final String validIccidRegex = "^89[f0-9]{17,22}$"; @@ -250,10 +270,10 @@ final public class CorrelationAttributeNormalizer { * @param data The string to normalize and validate * * @return the data with common number seperators removed if the data was - * determined to be a possible IMSI + * determined to be a possible IMSI * * @throws CorrelationAttributeNormalizationException if the data was not a - * valid IMSI + * valid IMSI */ private static String normalizeImsi(String data) throws CorrelationAttributeNormalizationException { final String validImsiRegex = "^[0-9]{14,15}$"; @@ -274,10 +294,10 @@ final public class CorrelationAttributeNormalizer { * @param data The string to normalize and validate * * @return the data with common number seperators removed and lowercased if - * the data was determined to be a possible MAC + * the data was determined to be a possible MAC * * @throws CorrelationAttributeNormalizationException if the data was not a - * valid MAC + * valid MAC */ private static String normalizeMac(String data) throws CorrelationAttributeNormalizationException { final String validMacRegex = "^([a-f0-9]{12}|[a-f0-9]{16})$"; @@ -303,10 +323,10 @@ final public class CorrelationAttributeNormalizer { * @param data The string to normalize and validate * * @return the data with common number seperators removed if the data was - * determined to be a possible IMEI + * determined to be a possible IMEI * * @throws CorrelationAttributeNormalizationException if the data was not a - * valid IMEI + * valid IMEI */ private static String normalizeImei(String data) throws CorrelationAttributeNormalizationException { final String validImeiRegex = "^[0-9]{14,16}$"; @@ -318,6 +338,58 @@ final public class CorrelationAttributeNormalizer { } } + // These symbols are allowed in written form of phone numbers. + // A '+' is allowed only as a leading digit and hence not inlcuded here. + // While a dialed sequence may have additonal special characters, such as #, * or ',', + // CR attributes represent accounts and hence those chatracter are not allowed. + private static final Set PHONENUMBER_CHARS = new HashSet<>(Arrays.asList( + "-", "(", ")" + )); + + private static final int MIN_PHONENUMBER_LEN = 5; + + /** + * Checks if the given string is a valid phone number. + * + * @param phoneNumber String to check. + * + * @return True if the given string is a valid phone number, false + * otherwise. + */ + static boolean isValidPhoneNumber(String phoneNumber) { + + // A phone number may have a leading '+', special telephony chars, or digits. + // Anything else implies an invalid phone number. + for (int i = 0; i < phoneNumber.length(); i++) { + if ( !((i == 0 && phoneNumber.charAt(i) == '+') + || Character.isSpaceChar(phoneNumber.charAt(i)) + || Character.isDigit(phoneNumber.charAt(i)) + || PHONENUMBER_CHARS.contains(String.valueOf(phoneNumber.charAt(i))))) { + return false; + } + } + + // ensure a min length + return phoneNumber.length() >= MIN_PHONENUMBER_LEN; + } + + /** + * Checks if the given string is a valid email address. + * + * @param emailAddress String to check. + * + * @return True if the given string is a valid email address, false + * otherwise. + */ + static boolean isValidEmailAddress(String emailAddress) { + if (!StringUtils.isEmpty(emailAddress)) { + EmailValidator validator = EmailValidator.getInstance(true, true); + return validator.isValid(emailAddress); + } + + return false; + } + /** * This is a utility class - no need for constructing or subclassing, etc... */ diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeUtil.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeUtil.java index c025308b00..9af6fcde3b 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeUtil.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeUtil.java @@ -34,8 +34,8 @@ import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; -import org.sleuthkit.datamodel.CommunicationsUtils; import org.sleuthkit.datamodel.HashUtility; +import org.sleuthkit.datamodel.InvalidAccountIDException; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; @@ -184,7 +184,15 @@ public class CorrelationAttributeUtil { makeCorrAttrsFromCommunicationArtifacts(correlationAttrs, sourceArtifact); } } - } catch (CentralRepoException ex) { + } catch (CorrelationAttributeNormalizationException ex) { + logger.log(Level.WARNING, String.format("Error normalizing correlation attribute (%s)", artifact), ex); // NON-NLS + return correlationAttrs; + } + catch (InvalidAccountIDException ex) { + logger.log(Level.WARNING, String.format("Invalid account identifier (artifactID: %d)", artifact.getId())); // NON-NLS + return correlationAttrs; + } + catch (CentralRepoException ex) { logger.log(Level.SEVERE, String.format("Error querying central repository (%s)", artifact), ex); // NON-NLS return correlationAttrs; } catch (TskCoreException ex) { @@ -198,18 +206,19 @@ public class CorrelationAttributeUtil { } /** - * Makes a correlation attribute instance from a phone number attribute of an - * artifact. + * Makes a correlation attribute instance from a phone number attribute of + * an artifact. * * @param corrAttrInstances Correlation attributes will be added to this. * @param artifact An artifact with a phone number attribute. * - * @throws TskCoreException If there is an error querying the case - * database. + * @throws TskCoreException If there is an error querying the case database. * @throws CentralRepoException If there is an error querying the central - * repository. + * repository. + * @throws CorrelationAttributeNormalizationException If there is an error + * in normalizing the attribute. */ - private static void makeCorrAttrsFromCommunicationArtifacts(List corrAttrInstances, BlackboardArtifact artifact) throws TskCoreException, CentralRepoException { + private static void makeCorrAttrsFromCommunicationArtifacts(List corrAttrInstances, BlackboardArtifact artifact) throws TskCoreException, CentralRepoException, CorrelationAttributeNormalizationException { CorrelationAttributeInstance corrAttr = null; /* @@ -227,13 +236,13 @@ public class CorrelationAttributeUtil { /* * Normalize the phone number. */ - if (value != null) { - if(CommunicationsUtils.isValidPhoneNumber(value)) { - value = CommunicationsUtils.normalizePhoneNum(value); - corrAttr = makeCorrAttr(artifact, CentralRepository.getInstance().getCorrelationTypeById(CorrelationAttributeInstance.PHONE_TYPE_ID), value); - if(corrAttr != null) { - corrAttrInstances.add(corrAttr); - } + if (value != null + && CorrelationAttributeNormalizer.isValidPhoneNumber(value)) { + + value = CorrelationAttributeNormalizer.normalizePhone(value); + corrAttr = makeCorrAttr(artifact, CentralRepository.getInstance().getCorrelationTypeById(CorrelationAttributeInstance.PHONE_TYPE_ID), value); + if (corrAttr != null) { + corrAttrInstances.add(corrAttr); } } } @@ -277,7 +286,7 @@ public class CorrelationAttributeUtil { * * @return The correlation attribute instance. */ - private static void makeCorrAttrFromAcctArtifact(List corrAttrInstances, BlackboardArtifact acctArtifact) throws TskCoreException, CentralRepoException { + private static void makeCorrAttrFromAcctArtifact(List corrAttrInstances, BlackboardArtifact acctArtifact) throws InvalidAccountIDException, TskCoreException, CentralRepoException { // Get the account type from the artifact BlackboardAttribute accountTypeAttribute = acctArtifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ACCOUNT_TYPE)); diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationDataSource.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationDataSource.java index ac17a1e962..92c23d77c8 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationDataSource.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationDataSource.java @@ -28,7 +28,7 @@ import org.sleuthkit.datamodel.TskDataException; /** * - * Stores information about a Data Source in the correlation engine + * Stores information about a Data Source in the Central Repository * */ public class CorrelationDataSource implements Serializable { diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/Persona.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/Persona.java index d5cd52c093..67ef828902 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/Persona.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/Persona.java @@ -24,11 +24,11 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.openide.util.NbBundle; -import org.sleuthkit.datamodel.SleuthkitCase; /** * This class abstracts a persona. @@ -42,11 +42,9 @@ public class Persona { * Defines level of confidence in assigning a persona to an account. */ public enum Confidence { - UNKNOWN(1, "Unknown"), - LOW(2, "Low confidence"), - MEDIUM(3, "Medium confidence"), - HIGH(4, "High confidence"), - DERIVED(5, "Derived directly"); + LOW(1, "Low confidence"), + MODERATE(2, "Moderate confidence"), + HIGH(3, "High confidence"); private final String name; private final int level_id; @@ -72,7 +70,7 @@ public class Persona { return confidence; } } - return Confidence.UNKNOWN; + return Confidence.LOW; } } @@ -124,7 +122,7 @@ public class Persona { private final long modifiedDate; private final PersonaStatus status; private final CentralRepoExaminer examiner; - + @NbBundle.Messages("Persona.defaultName=Unnamed") public static String getDefaultName() { return Bundle.Persona_defaultName(); @@ -202,16 +200,17 @@ public class Persona { /** * Creates a Persona and associates the specified account with it. * - * @param personaName Persona name. - * @param comment Comment to associate with persona, may be null. - * @param status Persona status - * @param account Account for which the persona is being created. + * @param personaName Persona name. + * @param comment Comment to associate with persona, may be null. + * @param status Persona status + * @param account Account for which the persona is being created. * @param justification Justification for why this account belongs to this - * persona, may be null. - * @param confidence Confidence level for this association of Persona & - * account. + * persona, may be null. + * @param confidence Confidence level for this association of Persona & + * account. * * @return Persona Persona created. + * * @throws CentralRepoException If there is an error creating the Persona. */ public static Persona createPersonaForAccount(String personaName, String comment, PersonaStatus status, CentralRepoAccount account, String justification, Persona.Confidence confidence) throws CentralRepoException { @@ -223,83 +222,133 @@ public class Persona { /** * Inserts a row in the Persona tables. * - * @param name Persona name, may be null - default name is used in that - * case. + * @param name Persona name, may be null - default name is used in that + * case. * @param comment Comment to associate with persona, may be null. - * @param status Persona status. + * @param status Persona status. * * @return Persona corresponding to the row inserted in the personas table. * * @throws CentralRepoException If there is an error in adding a row to - * personas table. + * personas table. */ private static Persona createPersona(String name, String comment, PersonaStatus status) throws CentralRepoException { // generate a UUID for the persona String uuidStr = UUID.randomUUID().toString(); - CentralRepoExaminer examiner = CentralRepository.getInstance().getOrInsertExaminer(System.getProperty("user.name")); + CentralRepoExaminer examiner = getCRInstance().getOrInsertExaminer(System.getProperty("user.name")); Instant instant = Instant.now(); Long timeStampMillis = instant.toEpochMilli(); - String insertClause = " INTO personas (uuid, comment, name, created_date, modified_date, status_id, examiner_id ) " - + "VALUES ( '" + uuidStr + "', " - + "'" + ((StringUtils.isBlank(comment) ? "" : SleuthkitCase.escapeSingleQuotes(comment))) + "'," - + "'" + ((StringUtils.isBlank(name) ? getDefaultName() : SleuthkitCase.escapeSingleQuotes(name))) + "'," - + timeStampMillis.toString() + "," - + timeStampMillis.toString() + "," - + status.getStatusId() + "," - + examiner.getId() - + ")"; - CentralRepository.getInstance().executeInsertSQL(insertClause); + String insertPersonaSQL = "INSERT INTO personas (uuid, comment, name, created_date, modified_date, status_id, examiner_id ) " //NON-NLS + + " VALUES (?, ?, ?, ?, ?, ?, ?)"; + List params = new ArrayList<>(); + params.add(uuidStr); + params.add(StringUtils.isBlank(comment) ? "" : comment); + params.add(StringUtils.isBlank(name) ? getDefaultName() : name); + params.add(timeStampMillis); + params.add(timeStampMillis); + params.add(status.getStatusId()); + params.add(examiner.getId()); + + getCRInstance().executeCommand(insertPersonaSQL, params); return getPersonaByUUID(uuidStr); } - + + /** + * Sets the comment of this persona. + * + * @param comment The new comment. + * + * @throws CentralRepoException If there is an error. + */ + public void setComment(String comment) throws CentralRepoException { + String updateSQL = "UPDATE personas SET comment = ? WHERE id = ?"; + CentralRepository cr = CentralRepository.getInstance(); + if (cr != null) { + List params = new ArrayList<>(); + params.add(StringUtils.isBlank(comment) ? "" : comment); + params.add(id); + + getCRInstance().executeCommand(updateSQL, params); + } + } + /** * Sets the name of this persona * * @param name The new name. - * + * * @throws CentralRepoException If there is an error. */ public void setName(String name) throws CentralRepoException { - String updateClause = "UPDATE personas SET name = \"" + name + "\" WHERE id = " + id; - CentralRepository.getInstance().executeUpdateSQL(updateClause); + String updateSQL = "UPDATE personas SET name = ? WHERE id = ?"; + CentralRepository cr = CentralRepository.getInstance(); + if (cr != null) { + List params = new ArrayList<>(); + params.add(StringUtils.isBlank(name) ? getDefaultName() : name); + params.add(id); + + getCRInstance().executeCommand(updateSQL, params); + } } /** * Associates an account with a persona by creating a row in the * PersonaAccounts table. * - * @param account Account to add to persona. + * @param account Account to add to persona. * @param justification Reason for adding the account to persona, may be - * null. - * @param confidence Confidence level. + * null. + * @param confidence Confidence level. * * @return PersonaAccount + * * @throws CentralRepoException If there is an error. */ public PersonaAccount addAccount(CentralRepoAccount account, String justification, Persona.Confidence confidence) throws CentralRepoException { return PersonaAccount.addPersonaAccount(this, account, justification, confidence); } - + /** * Removes the given PersonaAccount (persona/account association) * * @param account account to remove * * @throws CentralRepoException If there is an error in querying the - * Personas table. + * Personas table. */ public void removeAccount(PersonaAccount account) throws CentralRepoException { PersonaAccount.removePersonaAccount(account.getId()); } - + + /** + * Modifies the confidence / justification of the given PersonaAccount + * + * @param account Account to modify. + * @param confidence Level of confidence. + * @param justification Justification. + * + * @throws CentralRepoException If there is an error in querying the + * Personas table. + */ + public void modifyAccount(PersonaAccount account, Confidence confidence, String justification) throws CentralRepoException { + PersonaAccount.modifyPersonaAccount(account.getId(), confidence, justification); + } + /** * Marks this persona as deleted */ public void delete() throws CentralRepoException { - String deleteSQL = "UPDATE personas SET status_id = " + PersonaStatus.DELETED.status_id + " WHERE id = " + this.id; - CentralRepository.getInstance().executeUpdateSQL(deleteSQL); + String deleteSQL = "UPDATE personas SET status_id = ? WHERE id = ?"; + CentralRepository cr = CentralRepository.getInstance(); + if (cr != null) { + List params = new ArrayList<>(); + params.add(PersonaStatus.DELETED.getStatusId()); + params.add(id); + + getCRInstance().executeCommand(deleteSQL, params); + } } /** @@ -307,7 +356,7 @@ public class Persona { */ private static class PersonaQueryCallback implements CentralRepositoryDbQueryCallback { - private final Collection personaList = new ArrayList<>(); + private final Collection personaList = new ArrayList<>(); @Override public void process(ResultSet rs) throws SQLException { @@ -328,7 +377,7 @@ public class Persona { status, examiner ); - + personaList.add(persona); } } @@ -340,86 +389,166 @@ public class Persona { // Partial query string to select from personas table, // just supply the where clause. - private static final String PERSONA_QUERY = - "SELECT p.id, p.uuid, p.name, p.comment, p.created_date, p.modified_date, p.status_id, p.examiner_id, e.login_name, e.display_name " - + "FROM personas as p " - + "INNER JOIN examiners as e ON e.id = p.examiner_id "; - - + private static final String PERSONA_QUERY + = "SELECT p.id, p.uuid, p.name, p.comment, p.created_date, p.modified_date, p.status_id, p.examiner_id, e.login_name, e.display_name " + + "FROM personas as p " + + "INNER JOIN examiners as e ON e.id = p.examiner_id "; + /** * Gets the row from the Personas table with the given UUID, creates and * returns the Persona from that data. * * @param uuid Persona UUID to match. + * * @return Persona matching the given UUID, may be null if no match is - * found. + * found. * * @throws CentralRepoException If there is an error in querying the - * Personas table. + * Personas table. */ private static Persona getPersonaByUUID(String uuid) throws CentralRepoException { - String queryClause = - PERSONA_QUERY - + "WHERE p.uuid = '" + uuid + "'"; + String queryClause + = PERSONA_QUERY + + "WHERE p.uuid = ?"; + + List params = new ArrayList<>(); + params.add(uuid); PersonaQueryCallback queryCallback = new PersonaQueryCallback(); - CentralRepository.getInstance().executeSelectSQL(queryClause, queryCallback); + getCRInstance().executeQuery(queryClause, params, queryCallback); Collection personas = queryCallback.getPersonas(); - + return personas.isEmpty() ? null : personas.iterator().next(); } /** - * Gets the rows from the Personas table with matching name. - * Persona marked as DELETED are not returned. + * Gets the rows from the Personas table with matching name. Persona marked + * as DELETED are not returned. * * @param partialName Name substring to match. + * * @return Collection of personas matching the given name substring, may be - * empty if no match is found. + * empty if no match is found. * * @throws CentralRepoException If there is an error in querying the - * Personas table. + * Personas table. */ public static Collection getPersonaByName(String partialName) throws CentralRepoException { String queryClause = PERSONA_QUERY - + "WHERE p.status_id != " + PersonaStatus.DELETED.status_id + - " AND LOWER(p.name) LIKE " + "LOWER('%" + partialName + "%')" ; + + "WHERE p.status_id != ? " + + " AND LOWER(p.name) LIKE LOWER(?) ESCAPE '!'"; + + List params = new ArrayList<>(); + params.add(PersonaStatus.DELETED.getStatusId()); + params.add("%" + getLikeEscaped(partialName) + "%"); // partial substring search PersonaQueryCallback queryCallback = new PersonaQueryCallback(); - CentralRepository.getInstance().executeSelectSQL(queryClause, queryCallback); + getCRInstance().executeQuery(queryClause, params, queryCallback); return queryCallback.getPersonas(); } - + + /** + * Escapes string for use with like statements removing '%', '_', '\'. This + * uses '!' as the escape character and the sql should reflect this + * accordingly. See + * https://stackoverflow.com/questions/8247970/using-like-wildcard-in-prepared-statement, + * https://www.postgresql.org/docs/8.3/functions-matching.html and + * https://www.sqlite.org/lang_expr.html for more information. + * + * @param initial The initial string. + * + * @return The resulting string. + */ + private static String getLikeEscaped(String initial) { + if (initial == null) { + return null; + } + + return initial + .replace("!", "!!") + .replace("%", "!%") + .replace("_", "!_"); + } + + /** + * Gets the rows from the Personas table where persona accounts' names are + * similar to the given one. Persona marked as DELETED are not returned. + * + * @param partialName Name substring to match. + * + * @return Collection of personas matching the given name substring, may be + * empty if no match is found. + * + * @throws CentralRepoException If there is an error in querying the + * Personas table. + */ + public static Collection getPersonaByAccountIdentifierLike(String partialName) throws CentralRepoException { + String queryClause = "SELECT p.id, p.uuid, p.name, p.comment, p.created_date, p.modified_date, p.status_id, p.examiner_id, e.login_name\n" + + "FROM personas p\n" + + "LEFT JOIN examiners e ON e.id = p.examiner_id\n" + + "WHERE p.status_id <> ?\n" + + "AND p.id IN (\n" + + " SELECT pa.persona_id\n" + + " FROM persona_accounts pa\n" + + " INNER JOIN accounts a ON a.id = pa.account_id\n" + + " WHERE LOWER(a.account_unique_identifier) LIKE LOWER(?) ESCAPE '!'\n" + + ")"; + + PersonaQueryCallback queryCallback = new PersonaQueryCallback(); + + List params = new ArrayList<>(); + params.add(PersonaStatus.DELETED.getStatusId()); + params.add("%" + getLikeEscaped(partialName) + "%"); // partial substring search + + getCRInstance().executeQuery(queryClause, params, queryCallback); + return queryCallback.getPersonas(); + } + /** * Creates an alias for the Persona. * - * @param alias Alias name. + * @param alias Alias name. * @param justification Reason for assigning the alias, may be null. - * @param confidence Confidence level. + * @param confidence Confidence level. * * @return PersonaAlias + * * @throws CentralRepoException If there is an error in creating the alias. */ public PersonaAlias addAlias(String alias, String justification, Persona.Confidence confidence) throws CentralRepoException { return PersonaAlias.addPersonaAlias(this, alias, justification, confidence); } - + /** - * Removes the given alias + * Removes the given alias. * * @param alias alias to remove * * @throws CentralRepoException If there is an error in querying the - * Personas table. + * Personas table. */ public void removeAlias(PersonaAlias alias) throws CentralRepoException { PersonaAlias.removePersonaAlias(alias); } + /** + * Modifies the given alias. + * + * @param key Key for the alias to modify. + * @param confidence Level of confidence. + * @param justification Justification. + * + * @throws CentralRepoException If there is an error in querying the + * Personas table. + */ + public void modifyAlias(PersonaAlias key, Confidence confidence, String justification) throws CentralRepoException { + PersonaAlias.modifyPersonaAlias(key, confidence, justification); + } + /** * Gets all aliases for the persona. * @@ -434,30 +563,45 @@ public class Persona { /** * Adds specified metadata to the persona. * - * @param name Metadata name. - * @param value Metadata value. + * @param name Metadata name. + * @param value Metadata value. * @param justification Reason for adding the metadata, may be null. - * @param confidence Confidence level. + * @param confidence Confidence level. * * @return PersonaMetadata + * * @throws CentralRepoException If there is an error in adding metadata. */ public PersonaMetadata addMetadata(String name, String value, String justification, Persona.Confidence confidence) throws CentralRepoException { return PersonaMetadata.addPersonaMetadata(this.getId(), name, value, justification, confidence); } - + /** - * Removes the given metadata from this persona + * Removes the given metadata from this persona. * * @param metadata metadata to remove * * @throws CentralRepoException If there is an error in querying the - * Personas table. + * Personas table. */ public void removeMetadata(PersonaMetadata metadata) throws CentralRepoException { PersonaMetadata.removePersonaMetadata(metadata); } + /** + * Modifies the given metadata. + * + * @param key Key for the metadata to modify. + * @param confidence Level of confidence. + * @param justification Justification. + * + * @throws CentralRepoException If there is an error in querying the + * Personas table. + */ + public void modifyMetadata(PersonaMetadata key, Confidence confidence, String justification) throws CentralRepoException { + PersonaMetadata.modifyPersonaMetadata(key, confidence, justification); + } + /** * Gets all metadata for the persona. * @@ -475,12 +619,12 @@ public class Persona { * @return Collection of PersonaAccounts, may be empty. * * @throws CentralRepoException If there is an error in getting the - * persona_account. + * persona_account. */ public Collection getPersonaAccounts() throws CentralRepoException { return PersonaAccount.getPersonaAccountsForPersona(this.getId()); } - + /** * Callback to process a query that gets cases for account instances of an * account @@ -494,7 +638,7 @@ public class Persona { while (resultSet.next()) { // get Case for case_id - CorrelationCase correlationCase = CentralRepository.getInstance().getCaseById(resultSet.getInt("case_id")); + CorrelationCase correlationCase = getCRInstance().getCaseById(resultSet.getInt("case_id")); correlationCases.add(correlationCase); } } @@ -508,8 +652,9 @@ public class Persona { * Gets a list of cases that the persona appears in. * * @return Collection of cases that the persona appears in, may be empty. + * * @throws CentralRepoException If there is an error in getting the cases - * from the database. + * from the database. */ public Collection getCases() throws CentralRepoException { @@ -519,14 +664,17 @@ public class Persona { Collection accounts = PersonaAccount.getAccountsForPersona(this.getId()); for (CentralRepoAccount account : accounts) { int corrTypeId = account.getAccountType().getCorrelationTypeId(); - CorrelationAttributeInstance.Type correlationType = CentralRepository.getInstance().getCorrelationTypeById(corrTypeId); + CorrelationAttributeInstance.Type correlationType = getCRInstance().getCorrelationTypeById(corrTypeId); String tableName = CentralRepoDbUtil.correlationTypeToInstanceTableName(correlationType); String querySql = "SELECT DISTINCT case_id FROM " + tableName - + " WHERE account_id = " + account.getId(); + + " WHERE account_id = ?"; // param 1 + + List params = new ArrayList<>(); + params.add(account.getId()); CaseForAccountInstanceQueryCallback queryCallback = new CaseForAccountInstanceQueryCallback(); - CentralRepository.getInstance().executeSelectSQL(querySql, queryCallback); + getCRInstance().executeQuery(querySql, params, queryCallback); // Add any cases that aren't already on the list. for (CorrelationCase corrCase : queryCallback.getCases()) { @@ -553,8 +701,8 @@ public class Persona { while (resultSet.next()) { // get Case for case_id - CorrelationCase correlationCase = CentralRepository.getInstance().getCaseById(resultSet.getInt("case_id")); - CorrelationDataSource correlationDatasource = CentralRepository.getInstance().getDataSourceById(correlationCase, resultSet.getInt("data_source_id")); + CorrelationCase correlationCase = getCRInstance().getCaseById(resultSet.getInt("case_id")); + CorrelationDataSource correlationDatasource = getCRInstance().getDataSourceById(correlationCase, resultSet.getInt("data_source_id")); // Add data source to list if not already on it. if (!correlationDataSources.stream().anyMatch(p -> Objects.equals(p.getDataSourceObjectID(), correlationDatasource.getDataSourceObjectID()))) { @@ -572,7 +720,7 @@ public class Persona { * Gets all data sources that the persona appears in. * * @return Collection of data sources that the persona appears in, may be - * empty. + * empty. * * @throws CentralRepoException */ @@ -582,14 +730,17 @@ public class Persona { Collection accounts = PersonaAccount.getAccountsForPersona(this.getId()); for (CentralRepoAccount account : accounts) { int corrTypeId = account.getAccountType().getCorrelationTypeId(); - CorrelationAttributeInstance.Type correlationType = CentralRepository.getInstance().getCorrelationTypeById(corrTypeId); + CorrelationAttributeInstance.Type correlationType = getCRInstance().getCorrelationTypeById(corrTypeId); String tableName = CentralRepoDbUtil.correlationTypeToInstanceTableName(correlationType); String querySql = "SELECT case_id, data_source_id FROM " + tableName - + " WHERE account_id = " + account.getId(); + + " WHERE account_id = ?"; // param 1 + + List params = new ArrayList<>(); + params.add(account.getId()); DatasourceForAccountInstanceQueryCallback queryCallback = new DatasourceForAccountInstanceQueryCallback(); - CentralRepository.getInstance().executeSelectSQL(querySql, queryCallback); + getCRInstance().executeQuery(querySql, params, queryCallback); // Add any data sources that aren't already on the list. for (CorrelationDataSource correlationDatasource : queryCallback.getDataSources()) { @@ -646,13 +797,15 @@ public class Persona { * the X_instance table for the given account type. * * @param crAccountType Account type to generate the query string for. + * * @return Query substring. + * * @throws CentralRepoException */ private static String getPersonaFromInstanceTableQueryTemplate(CentralRepoAccount.CentralRepoAccountType crAccountType) throws CentralRepoException { int corrTypeId = crAccountType.getCorrelationTypeId(); - CorrelationAttributeInstance.Type correlationType = CentralRepository.getInstance().getCorrelationTypeById(corrTypeId); + CorrelationAttributeInstance.Type correlationType = getCRInstance().getCorrelationTypeById(corrTypeId); String instanceTableName = CentralRepoDbUtil.correlationTypeToInstanceTableName(correlationType); return "SELECT " + instanceTableName + ".account_id, case_id, data_source_id, " @@ -671,20 +824,25 @@ public class Persona { * @param correlationCase Case to look the persona in. * * @return Collection of personas, may be empty. + * * @throws CentralRepoException */ public static Collection getPersonasForCase(CorrelationCase correlationCase) throws CentralRepoException { Collection personaList = new ArrayList<>(); - Collection accountTypes = CentralRepository.getInstance().getAllAccountTypes(); + Collection accountTypes = getCRInstance().getAllAccountTypes(); for (CentralRepoAccount.CentralRepoAccountType crAccountType : accountTypes) { String querySql = getPersonaFromInstanceTableQueryTemplate(crAccountType) - + " WHERE case_id = " + correlationCase.getID() - + "AND personas.status_id != " + Persona.PersonaStatus.DELETED.getStatusId(); + + " WHERE case_id = ?" // param 1 + + " AND personas.status_id != ?"; // param 2 + + List params = new ArrayList<>(); + params.add(correlationCase.getID()); + params.add(Persona.PersonaStatus.DELETED.getStatusId()); PersonaFromAccountInstanceQueryCallback queryCallback = new PersonaFromAccountInstanceQueryCallback(); - CentralRepository.getInstance().executeSelectSQL(querySql, queryCallback); + getCRInstance().executeQuery(querySql, params, queryCallback); // Add persona that aren't already on the list. for (Persona persona : queryCallback.getPersonasList()) { @@ -703,20 +861,25 @@ public class Persona { * @param dataSource Data source to look the persona in. * * @return Collection of personas, may be empty. + * * @throws CentralRepoException */ public static Collection getPersonasForDataSource(CorrelationDataSource dataSource) throws CentralRepoException { Collection personaList = new ArrayList<>(); - Collection accountTypes = CentralRepository.getInstance().getAllAccountTypes(); + Collection accountTypes = getCRInstance().getAllAccountTypes(); for (CentralRepoAccount.CentralRepoAccountType crAccountType : accountTypes) { String querySql = getPersonaFromInstanceTableQueryTemplate(crAccountType) - + " WHERE data_source_id = " + dataSource.getID() - + "AND personas.status_id != " + Persona.PersonaStatus.DELETED.getStatusId(); + + " WHERE data_source_id = ?" + + " AND personas.status_id != ?"; + + List params = new ArrayList<>(); + params.add(dataSource.getID()); + params.add(Persona.PersonaStatus.DELETED.getStatusId()); PersonaFromAccountInstanceQueryCallback queryCallback = new PersonaFromAccountInstanceQueryCallback(); - CentralRepository.getInstance().executeSelectSQL(querySql, queryCallback); + getCRInstance().executeQuery(querySql, params, queryCallback); // Add persona that aren't already on the list. for (Persona persona : queryCallback.getPersonasList()) { @@ -728,4 +891,22 @@ public class Persona { } return personaList; } + + /** + * Wraps the call to CentralRepository.getInstance() throwing an exception + * if instance is null; + * + * @return Instance of CentralRepository + * + * @throws CentralRepoException + */ + private static CentralRepository getCRInstance() throws CentralRepoException { + CentralRepository instance = CentralRepository.getInstance(); + + if (instance == null) { + throw new CentralRepoException("Failed to get instance of CentralRespository, CR was null"); + } + + return instance; + } } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PersonaAccount.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PersonaAccount.java index d0322ddbca..917c988e1c 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PersonaAccount.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PersonaAccount.java @@ -24,9 +24,9 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Objects; import org.apache.commons.lang3.StringUtils; -import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.Account; /** @@ -127,38 +127,41 @@ public class PersonaAccount { * @param confidence Confidence level. * * @return PersonaAccount + * * @throws CentralRepoException If there is an error in creating the * account. */ static PersonaAccount addPersonaAccount(Persona persona, CentralRepoAccount account, String justification, Persona.Confidence confidence) throws CentralRepoException { - CentralRepository cr = CentralRepository.getInstance(); - - if(cr == null) { - throw new CentralRepoException("Failed to add Persona, Central Repository is not enable"); - } - - CentralRepoExaminer currentExaminer = cr.getOrInsertExaminer(System.getProperty("user.name")); + CentralRepoExaminer currentExaminer = getCRInstance().getOrInsertExaminer(System.getProperty("user.name")); Instant instant = Instant.now(); Long timeStampMillis = instant.toEpochMilli(); - String insertClause = " INTO persona_accounts (persona_id, account_id, justification, confidence_id, date_added, examiner_id ) " - + "VALUES ( " - + persona.getId() + ", " - + account.getId() + ", " - + "'" + ((StringUtils.isBlank(justification) ? "" : SleuthkitCase.escapeSingleQuotes(justification))) + "', " - + confidence.getLevelId() + ", " - + timeStampMillis.toString() + ", " - + currentExaminer.getId() - + ")"; - CentralRepository.getInstance().executeInsertSQL(insertClause); + String insertSQL = "INSERT INTO persona_accounts (persona_id, account_id, justification, confidence_id, date_added, examiner_id ) " + + " VALUES ( ?, ?, ?, ?, ?, ?)"; + + List params = new ArrayList<>(); + params.add(persona.getId()); + params.add(account.getId()); + params.add(StringUtils.isBlank(justification) ? "" : justification); + params.add(confidence.getLevelId()); + params.add(timeStampMillis); + params.add(currentExaminer.getId()); + + getCRInstance().executeCommand(insertSQL, params); + + String querySQL = PERSONA_ACCOUNTS_QUERY_CLAUSE + + "WHERE persona_id = ? " + + " AND account_type_id = ?" + + " AND account_unique_identifier = ?"; + + List queryParams = new ArrayList<>(); + queryParams.add(persona.getId()); + queryParams.add(account.getAccountType().getAccountTypeId()); + queryParams.add(account.getIdentifier()); - String queryClause = PERSONA_ACCOUNTS_QUERY_CALUSE - + "WHERE persona_id = " + persona.getId() - + " AND account_type_id = " + account.getAccountType().getAccountTypeId() - + " AND account_unique_identifier = \"" + account.getIdentifier() + "\""; PersonaAccountsQueryCallback queryCallback = new PersonaAccountsQueryCallback(); - CentralRepository.getInstance().executeSelectSQL(queryClause, queryCallback); + getCRInstance().executeQuery(querySQL, queryParams, queryCallback); Collection accounts = queryCallback.getPersonaAccountsList(); if (accounts.size() != 1) { @@ -203,7 +206,7 @@ public class PersonaAccount { ); // create account - CentralRepoAccount.CentralRepoAccountType crAccountType = CentralRepository.getInstance().getAccountTypeByName(rs.getString("type_name")); + CentralRepoAccount.CentralRepoAccountType crAccountType = getCRInstance().getAccountTypeByName(rs.getString("type_name")); CentralRepoAccount account = new CentralRepoAccount( rs.getInt("account_id"), crAccountType, @@ -226,7 +229,7 @@ public class PersonaAccount { }; // Query clause to select from persona_accounts table to create PersonaAccount(s) - private static final String PERSONA_ACCOUNTS_QUERY_CALUSE = "SELECT persona_accounts.id as persona_accounts_id, justification, confidence_id, date_added, persona_accounts.examiner_id as pa_examiner_id, pa_examiner.login_name as pa_examiner_login_name, pa_examiner.display_name as pa_examiner_display_name," + private static final String PERSONA_ACCOUNTS_QUERY_CLAUSE = "SELECT persona_accounts.id as persona_accounts_id, justification, confidence_id, date_added, persona_accounts.examiner_id as pa_examiner_id, pa_examiner.login_name as pa_examiner_login_name, pa_examiner.display_name as pa_examiner_display_name," + " personas.id as persona_id, personas.uuid, personas.name, personas.comment, personas.created_date, personas.modified_date, personas.status_id, " + " personas.examiner_id as persona_examiner_id, persona_examiner.login_name as persona_examiner_login_name, persona_examiner.display_name as persona_examiner_display_name, " + " accounts.id as account_id, account_type_id, account_unique_identifier," @@ -246,22 +249,19 @@ public class PersonaAccount { * @return Collection of PersonaAccounts, may be empty. * * @throws CentralRepoException If there is an error in getting the - * persona_account. + * persona_account. */ static Collection getPersonaAccountsForPersona(long personaId) throws CentralRepoException { - CentralRepository cr = CentralRepository.getInstance(); + String querySQL = PERSONA_ACCOUNTS_QUERY_CLAUSE + + " WHERE persona_accounts.persona_id = ?"; - if (cr != null) { - String queryClause = PERSONA_ACCOUNTS_QUERY_CALUSE - + " WHERE persona_accounts.persona_id = " + personaId; + List queryParams = new ArrayList<>(); + queryParams.add(personaId); - PersonaAccountsQueryCallback queryCallback = new PersonaAccountsQueryCallback(); - cr.executeSelectSQL(queryClause, queryCallback); + PersonaAccountsQueryCallback queryCallback = new PersonaAccountsQueryCallback(); + getCRInstance().executeQuery(querySQL, queryParams, queryCallback); - return queryCallback.getPersonaAccountsList(); - } - - return new ArrayList<>(); + return queryCallback.getPersonaAccountsList(); } /** @@ -272,23 +272,20 @@ public class PersonaAccount { * @return Collection of PersonaAccounts. may be empty. * * @throws CentralRepoException If there is an error in getting the - * persona_account. + * persona_account. */ public static Collection getPersonaAccountsForAccount(long accountId) throws CentralRepoException { - String queryClause = PERSONA_ACCOUNTS_QUERY_CALUSE - + " WHERE persona_accounts.account_id = " + accountId - + "AND p.status_id != " + Persona.PersonaStatus.DELETED.getStatusId(); + String querySQL = PERSONA_ACCOUNTS_QUERY_CLAUSE + + " WHERE persona_accounts.account_id = ?" + + " AND personas.status_id != ?"; - CentralRepository cr = CentralRepository.getInstance(); + List queryParams = new ArrayList<>(); + queryParams.add(accountId); + queryParams.add(Persona.PersonaStatus.DELETED.getStatusId()); - if (cr != null) { - PersonaAccountsQueryCallback queryCallback = new PersonaAccountsQueryCallback(); - cr.executeSelectSQL(queryClause, queryCallback); - - return queryCallback.getPersonaAccountsList(); - } - - return new ArrayList<>(); + PersonaAccountsQueryCallback queryCallback = new PersonaAccountsQueryCallback(); + getCRInstance().executeQuery(querySQL, queryParams, queryCallback); + return queryCallback.getPersonaAccountsList(); } /** @@ -296,27 +293,25 @@ public class PersonaAccount { * account identifier substring. * * @param accountIdentifierSubstring Account identifier substring to search - * for. + * for. * * @return Collection of PersonaAccounts. may be empty. * * @throws CentralRepoException If there is an error in getting the - * persona_account. + * persona_account. */ public static Collection getPersonaAccountsForIdentifierLike(String accountIdentifierSubstring) throws CentralRepoException { - String queryClause = PERSONA_ACCOUNTS_QUERY_CALUSE - + " WHERE LOWER(accounts.account_unique_identifier) LIKE LOWER('%" + accountIdentifierSubstring + "%')" - + "AND p.status_id != " + Persona.PersonaStatus.DELETED.getStatusId(); + String querySQL = PERSONA_ACCOUNTS_QUERY_CLAUSE + + " WHERE LOWER(accounts.account_unique_identifier) LIKE LOWER(?)" + + " AND personas.status_id != ?"; - CentralRepository cr = CentralRepository.getInstance(); - if (cr != null) { - PersonaAccountsQueryCallback queryCallback = new PersonaAccountsQueryCallback(); - cr.executeSelectSQL(queryClause, queryCallback); + List queryParams = new ArrayList<>(); + queryParams.add("%" + accountIdentifierSubstring + "%"); // substring match + queryParams.add(Persona.PersonaStatus.DELETED.getStatusId()); - return queryCallback.getPersonaAccountsList(); - } - - return new ArrayList<>(); + PersonaAccountsQueryCallback queryCallback = new PersonaAccountsQueryCallback(); + getCRInstance().executeQuery(querySQL, queryParams, queryCallback); + return queryCallback.getPersonaAccountsList(); } /** @@ -325,22 +320,24 @@ public class PersonaAccount { * @param account Account to search for. * * @return Collection of PersonaAccounts, maybe empty if none were found or - * CR is not enabled. + * CR is not enabled. * * @throws CentralRepoException */ public static Collection getPersonaAccountsForAccount(Account account) throws CentralRepoException { - String queryClause = PERSONA_ACCOUNTS_QUERY_CALUSE - + " WHERE LOWER(accounts.account_unique_identifier) LIKE LOWER('%" + account.getTypeSpecificID() + "%') AND type_name = '" + account.getAccountType().getTypeName() + "' "; + String querySQL = PERSONA_ACCOUNTS_QUERY_CLAUSE + + " WHERE LOWER(accounts.account_unique_identifier) LIKE LOWER(?)" + + " AND type_name = ?" + + " AND personas.status_id != ?"; - CentralRepository cr = CentralRepository.getInstance(); - if (cr != null) { - PersonaAccountsQueryCallback queryCallback = new PersonaAccountsQueryCallback(); - cr.executeSelectSQL(queryClause, queryCallback); - return queryCallback.getPersonaAccountsList(); - } + List queryParams = new ArrayList<>(); + queryParams.add("%" + account.getTypeSpecificID() + "%"); // substring match + queryParams.add(account.getAccountType().getTypeName()); + queryParams.add(Persona.PersonaStatus.DELETED.getStatusId()); - return new ArrayList<>(); + PersonaAccountsQueryCallback queryCallback = new PersonaAccountsQueryCallback(); + getCRInstance().executeQuery(querySQL, queryParams, queryCallback); + return queryCallback.getPersonaAccountsList(); } /** @@ -352,14 +349,30 @@ public class PersonaAccount { * account. */ static void removePersonaAccount(long id) throws CentralRepoException { - CentralRepository cr = CentralRepository.getInstance(); - - if(cr == null) { - throw new CentralRepoException("Failed to remove persona account, Central Repo is not enabled"); - } - - String deleteClause = " DELETE FROM persona_accounts WHERE id = " + id; - cr.executeDeleteSQL(deleteClause); + String deleteSQL = " DELETE FROM persona_accounts WHERE id = ?"; + List params = new ArrayList<>(); + params.add(id); + + getCRInstance().executeCommand(deleteSQL, params); + } + + /** + * Modifies the PersonaAccount row by the given id + * + * @param id row id for the account to be removed + * + * @throws CentralRepoException If there is an error in removing the + * account. + */ + static void modifyPersonaAccount(long id, Persona.Confidence confidence, String justification) throws CentralRepoException { + String updateSQL = "UPDATE persona_accounts SET confidence_id = ?, justification = ? WHERE id = ?"; + + List params = new ArrayList<>(); + params.add(confidence.getLevelId()); + params.add(StringUtils.isBlank(justification) ? "" : justification); + params.add(id); + + getCRInstance().executeCommand(updateSQL, params); } /** @@ -376,7 +389,7 @@ public class PersonaAccount { while (rs.next()) { // create account - CentralRepoAccount.CentralRepoAccountType crAccountType = CentralRepository.getInstance().getAccountTypeByName(rs.getString("type_name")); + CentralRepoAccount.CentralRepoAccountType crAccountType = getCRInstance().getAccountTypeByName(rs.getString("type_name")); CentralRepoAccount account = new CentralRepoAccount( rs.getInt("account_id"), crAccountType, @@ -398,27 +411,43 @@ public class PersonaAccount { * * @return Collection of all accounts associated with the given persona, may * be empty. + * * @throws CentralRepoException If there is an error in getting the * accounts. */ static Collection getAccountsForPersona(long personaId) throws CentralRepoException { - CentralRepository cr = CentralRepository.getInstance(); + String queryClause = "SELECT account_id, " + + " accounts.account_type_id as account_type_id, accounts.account_unique_identifier as account_unique_identifier," + + " account_types.type_name as type_name " + + " FROM persona_accounts " + + " JOIN accounts as accounts on persona_accounts.account_id = accounts.id " + + " JOIN account_types as account_types on accounts.account_type_id = account_types.id " + + " WHERE persona_accounts.persona_id = ?"; - if (cr != null) { - String queryClause = "SELECT account_id, " - + " accounts.account_type_id as account_type_id, accounts.account_unique_identifier as account_unique_identifier," - + " account_types.type_name as type_name " - + " FROM persona_accounts " - + " JOIN accounts as accounts on persona_accounts.account_id = accounts.id " - + " JOIN account_types as account_types on accounts.account_type_id = account_types.id " - + " WHERE persona_accounts.persona_id = " + personaId; + List queryParams = new ArrayList<>(); + queryParams.add(personaId); - AccountsForPersonaQueryCallback queryCallback = new AccountsForPersonaQueryCallback(); - cr.executeSelectSQL(queryClause, queryCallback); + AccountsForPersonaQueryCallback queryCallback = new AccountsForPersonaQueryCallback(); + getCRInstance().executeQuery(queryClause, queryParams, queryCallback); - return queryCallback.getAccountsList(); + return queryCallback.getAccountsList(); + } + + /** + * Wraps the call to CentralRepository.getInstance() throwing an exception + * if instance is null; + * + * @return Instance of CentralRepository + * + * @throws CentralRepoException + */ + private static CentralRepository getCRInstance() throws CentralRepoException { + CentralRepository instance = CentralRepository.getInstance(); + + if (instance == null) { + throw new CentralRepoException("Failed to get instance of CentralRespository, CR was null"); } - return new ArrayList<>(); + return instance; } } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PersonaAlias.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PersonaAlias.java index 12818d45da..d2df0c63d5 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PersonaAlias.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PersonaAlias.java @@ -24,21 +24,21 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; import org.apache.commons.lang3.StringUtils; -import org.sleuthkit.datamodel.SleuthkitCase; /** - * This class abstracts an alias assigned to a Persona. - * A Persona may have multiple aliases. - * + * This class abstracts an alias assigned to a Persona. A Persona may have + * multiple aliases. + * */ public class PersonaAlias { - - private static final String SELECT_QUERY_BASE = - "SELECT pa.id, pa.persona_id, pa.alias, pa.justification, pa.confidence_id, pa.date_added, pa.examiner_id, e.login_name, e.display_name " - + "FROM persona_alias as pa " - + "INNER JOIN examiners as e ON e.id = pa.examiner_id "; - + + private static final String SELECT_QUERY_BASE + = "SELECT pa.id, pa.persona_id, pa.alias, pa.justification, pa.confidence_id, pa.date_added, pa.examiner_id, e.login_name, e.display_name " + + "FROM persona_alias as pa " + + "INNER JOIN examiners as e ON e.id = pa.examiner_id "; + private final long id; private final long personaId; private final String alias; @@ -46,7 +46,7 @@ public class PersonaAlias { private final Persona.Confidence confidence; private final long dateAdded; private final CentralRepoExaminer examiner; - + public long getId() { return id; } @@ -74,7 +74,7 @@ public class PersonaAlias { public CentralRepoExaminer getExaminer() { return examiner; } - + public PersonaAlias(long id, long personaId, String alias, String justification, Persona.Confidence confidence, long dateAdded, CentralRepoExaminer examiner) { this.id = id; this.personaId = personaId; @@ -84,8 +84,8 @@ public class PersonaAlias { this.dateAdded = dateAdded; this.examiner = examiner; } - - /** + + /** * Creates an alias for the specified Persona. * * @param persona Persona for which the alias is being added. @@ -98,40 +98,47 @@ public class PersonaAlias { */ static PersonaAlias addPersonaAlias(Persona persona, String alias, String justification, Persona.Confidence confidence) throws CentralRepoException { - CentralRepoExaminer examiner = CentralRepository.getInstance().getOrInsertExaminer(System.getProperty("user.name")); + CentralRepoExaminer examiner = getCRInstance().getOrInsertExaminer(System.getProperty("user.name")); Instant instant = Instant.now(); Long timeStampMillis = instant.toEpochMilli(); - String insertClause = " INTO persona_alias (persona_id, alias, justification, confidence_id, date_added, examiner_id ) " - + "VALUES ( " - + persona.getId() + ", " - + "'" + alias + "', " - + "'" + ((StringUtils.isBlank(justification) ? "" : SleuthkitCase.escapeSingleQuotes(justification))) + "', " - + confidence.getLevelId() + ", " - + timeStampMillis.toString() + ", " - + examiner.getId() - + ")"; + String insertSQL = "INSERT INTO persona_alias (persona_id, alias, justification, confidence_id, date_added, examiner_id ) " + + " VALUES ( ?, ?, ?, ?, ?, ?)"; + + List params = new ArrayList<>(); + params.add(persona.getId()); + params.add(alias); + params.add(StringUtils.isBlank(justification) ? "" : justification); + params.add(confidence.getLevelId()); + params.add(timeStampMillis); + params.add(examiner.getId()); + + getCRInstance().executeCommand(insertSQL, params); - CentralRepository.getInstance().executeInsertSQL(insertClause); - String queryClause = SELECT_QUERY_BASE - + "WHERE pa.persona_id = " + persona.getId() - + " AND pa.alias = \"" + alias + "\"" - + " AND pa.date_added = " + timeStampMillis - + " AND pa.examiner_id = " + examiner.getId(); - + + "WHERE pa.persona_id = ?" + + " AND pa.alias = ?" + + " AND pa.date_added = ?" + + " AND pa.examiner_id = ?"; + + List queryParams = new ArrayList<>(); + queryParams.add(persona.getId()); + queryParams.add(alias); + queryParams.add(timeStampMillis); + queryParams.add(examiner.getId()); + PersonaAliasesQueryCallback queryCallback = new PersonaAliasesQueryCallback(); - CentralRepository.getInstance().executeSelectSQL(queryClause, queryCallback); - + getCRInstance().executeQuery(queryClause, queryParams, queryCallback); + Collection aliases = queryCallback.getAliases(); if (aliases.size() != 1) { throw new CentralRepoException("Alias add query failed"); } - + return aliases.iterator().next(); } - + /** * Removes a PersonaAlias. * @@ -140,11 +147,39 @@ public class PersonaAlias { * @throws CentralRepoException If there is an error in removing the alias. */ static void removePersonaAlias(PersonaAlias alias) throws CentralRepoException { - String deleteClause = " DELETE FROM persona_alias WHERE id = " + alias.getId(); - CentralRepository.getInstance().executeDeleteSQL(deleteClause); + String deleteSQL = " DELETE FROM persona_alias WHERE id = ?"; + + List params = new ArrayList<>(); + params.add(alias.getId()); + + getCRInstance().executeCommand(deleteSQL, params); } - - /** + + /** + * Modifies a PesronaAlias. + * + * @param alias Alias to modify. + * + * @throws CentralRepoException If there is an error in modifying the alias. + */ + static void modifyPersonaAlias(PersonaAlias alias, Persona.Confidence confidence, String justification) throws CentralRepoException { + CentralRepository cr = CentralRepository.getInstance(); + + if (cr == null) { + throw new CentralRepoException("Failed to modify persona alias, Central Repo is not enabled"); + } + + String updateClause = "UPDATE persona_alias SET confidence_id = ?, justification = ? WHERE id = ?"; + + List params = new ArrayList<>(); + params.add(confidence.getLevelId()); + params.add(StringUtils.isBlank(justification) ? "" : justification); + params.add(alias.getId()); + + cr.executeCommand(updateClause, params); + } + + /** * Callback to process a Persona aliases query. */ static class PersonaAliasesQueryCallback implements CentralRepositoryDbQueryCallback { @@ -176,7 +211,7 @@ public class PersonaAlias { return Collections.unmodifiableCollection(personaAliases); } }; - + /** * Gets all aliases for the persona with specified id. * @@ -186,12 +221,33 @@ public class PersonaAlias { * @throws CentralRepoException If there is an error in retrieving aliases. */ public static Collection getPersonaAliases(long personaId) throws CentralRepoException { - String queryClause = SELECT_QUERY_BASE + "WHERE pa.persona_id = " + personaId; + String queryClause = SELECT_QUERY_BASE + + "WHERE pa.persona_id = ?"; + + List params = new ArrayList<>(); + params.add(personaId); PersonaAliasesQueryCallback queryCallback = new PersonaAliasesQueryCallback(); - CentralRepository.getInstance().executeSelectSQL(queryClause, queryCallback); + getCRInstance().executeQuery(queryClause, params, queryCallback); return queryCallback.getAliases(); } - + + /** + * Wraps the call to CentralRepository.getInstance() throwing an exception + * if instance is null; + * + * @return Instance of CentralRepository + * + * @throws CentralRepoException + */ + private static CentralRepository getCRInstance() throws CentralRepoException { + CentralRepository instance = CentralRepository.getInstance(); + + if (instance == null) { + throw new CentralRepoException("Failed to get instance of CentralRespository, CR was null"); + } + + return instance; + } } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PersonaMetadata.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PersonaMetadata.java index 5c3b9a900d..55830e9c54 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PersonaMetadata.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PersonaMetadata.java @@ -24,23 +24,23 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; import org.apache.commons.lang3.StringUtils; -import org.sleuthkit.datamodel.SleuthkitCase; /** - * This class abstracts metadata associated with a Persona. - * Metadata is in the form of a name/value pair. - * + * This class abstracts metadata associated with a Persona. Metadata is in the + * form of a name/value pair. + * * A Persona may have zero or more metadata. - * + * */ public class PersonaMetadata { - - private static final String SELECT_QUERY_BASE = - "SELECT pmd.id, pmd.persona_id, pmd.name, pmd.value, pmd.justification, pmd.confidence_id, pmd.date_added, pmd.examiner_id, e.login_name, e.display_name " - + "FROM persona_metadata as pmd " - + "INNER JOIN examiners as e ON e.id = pmd.examiner_id "; - + + private static final String SELECT_QUERY_BASE + = "SELECT pmd.id, pmd.persona_id, pmd.name, pmd.value, pmd.justification, pmd.confidence_id, pmd.date_added, pmd.examiner_id, e.login_name, e.display_name " + + "FROM persona_metadata as pmd " + + "INNER JOIN examiners as e ON e.id = pmd.examiner_id "; + private final long id; private final long personaId; private final String name; @@ -49,7 +49,7 @@ public class PersonaMetadata { private final Persona.Confidence confidence; private final long dateAdded; private final CentralRepoExaminer examiner; - + public long getId() { return id; } @@ -81,7 +81,7 @@ public class PersonaMetadata { public CentralRepoExaminer getExaminer() { return examiner; } - + public PersonaMetadata(long id, long personaId, String name, String value, String justification, Persona.Confidence confidence, long dateAdded, CentralRepoExaminer examiner) { this.id = id; this.personaId = personaId; @@ -92,8 +92,8 @@ public class PersonaMetadata { this.dateAdded = dateAdded; this.examiner = examiner; } - - /** + + /** * Adds specified metadata to the given persona. * * @param personaId Id of persona to add metadata for. @@ -107,54 +107,92 @@ public class PersonaMetadata { */ static PersonaMetadata addPersonaMetadata(long personaId, String name, String value, String justification, Persona.Confidence confidence) throws CentralRepoException { - CentralRepoExaminer examiner = CentralRepository.getInstance().getOrInsertExaminer(System.getProperty("user.name")); + CentralRepoExaminer examiner = getCRInstance().getOrInsertExaminer(System.getProperty("user.name")); Instant instant = Instant.now(); Long timeStampMillis = instant.toEpochMilli(); - String insertClause = " INTO persona_metadata (persona_id, name, value, justification, confidence_id, date_added, examiner_id ) " - + "VALUES ( " - + personaId + ", " - + "'" + name + "', " - + "'" + value + "', " - + "'" + ((StringUtils.isBlank(justification) ? "" : SleuthkitCase.escapeSingleQuotes(justification))) + "', " - + confidence.getLevelId() + ", " - + timeStampMillis.toString() + ", " - + examiner.getId() - + ")"; + String insertSQL = "INSERT INTO persona_metadata (persona_id, name, value, justification, confidence_id, date_added, examiner_id ) " + + "VALUES ( ?, ?, ?, ?, ?, ?, ?)"; + + List params = new ArrayList<>(); + params.add(personaId); + params.add(name); + params.add(value); + params.add(StringUtils.isBlank(justification) ? "" : justification); + params.add(confidence.getLevelId()); + params.add(timeStampMillis); + params.add(examiner.getId()); + + getCRInstance().executeCommand(insertSQL, params); - CentralRepository.getInstance().executeInsertSQL(insertClause); - String queryClause = SELECT_QUERY_BASE - + "WHERE pmd.persona_id = " + personaId - + " AND pmd.name = \"" + name + "\"" - + " AND pmd.value = \"" + value + "\"" - + " AND pmd.date_added = " + timeStampMillis - + " AND pmd.examiner_id = " + examiner.getId(); - + + "WHERE pmd.persona_id = ?" + + " AND pmd.name = ?" + + " AND pmd.value = ?" + + " AND pmd.date_added = ?" + + " AND pmd.examiner_id = ?"; + + List queryParams = new ArrayList<>(); + queryParams.add(personaId); + queryParams.add(name); + queryParams.add(value); + queryParams.add(timeStampMillis); + queryParams.add(examiner.getId()); + PersonaMetadataQueryCallback queryCallback = new PersonaMetadataQueryCallback(); - CentralRepository.getInstance().executeSelectSQL(queryClause, queryCallback); - + getCRInstance().executeQuery(queryClause, queryParams, queryCallback); + Collection metadata = queryCallback.getMetadataList(); if (metadata.size() != 1) { throw new CentralRepoException("Metadata add query failed"); } - + return metadata.iterator().next(); } - + /** * Removes the given PersonaMetadata * * @param metadata Metadata to remove. * - * @throws CentralRepoException If there is an error in removing the metadata. + * @throws CentralRepoException If there is an error in removing the + * metadata. */ static void removePersonaMetadata(PersonaMetadata metadata) throws CentralRepoException { - String deleteClause = " DELETE FROM persona_metadata WHERE id = " + metadata.getId(); - CentralRepository.getInstance().executeDeleteSQL(deleteClause); + String deleteSql = " DELETE FROM persona_metadata WHERE id = ?"; + + List params = new ArrayList<>(); + params.add(metadata.getId()); + + getCRInstance().executeCommand(deleteSql, params); } - + + /** + * Modifies the given PersonaMetadata + * + * @param metadata Metadata to modify. + * + * @throws CentralRepoException If there is an error in modifying the + * metadata. + */ + static void modifyPersonaMetadata(PersonaMetadata metadata, Persona.Confidence confidence, String justification) throws CentralRepoException { + CentralRepository cr = CentralRepository.getInstance(); + + if (cr == null) { + throw new CentralRepoException("Failed to modify persona metadata, Central Repo is not enabled"); + } + + String updateSql = "UPDATE persona_metadata SET confidence_id = ?, justification = ? WHERE id = ?"; + + List params = new ArrayList<>(); + params.add(confidence.getLevelId()); + params.add(StringUtils.isBlank(justification) ? "" : justification); + params.add(metadata.id); + + getCRInstance().executeCommand(updateSql, params); + } + /** * Callback to process a Persona metadata query. */ @@ -188,8 +226,8 @@ public class PersonaMetadata { return Collections.unmodifiableCollection(personaMetadataList); } }; - - /** + + /** * Gets all metadata for the persona with specified id. * * @param personaId Id of the persona for which to get the metadata. @@ -198,13 +236,34 @@ public class PersonaMetadata { * @throws CentralRepoException If there is an error in retrieving aliases. */ static Collection getPersonaMetadata(long personaId) throws CentralRepoException { - String queryClause = SELECT_QUERY_BASE + "WHERE pmd.persona_id = " + personaId; - + String queryClause = SELECT_QUERY_BASE + + "WHERE pmd.persona_id = ?"; + + List params = new ArrayList<>(); + params.add(personaId); + PersonaMetadataQueryCallback queryCallback = new PersonaMetadataQueryCallback(); - CentralRepository.getInstance().executeSelectSQL(queryClause, queryCallback); + getCRInstance().executeQuery(queryClause, params, queryCallback); return queryCallback.getMetadataList(); } - + + /** + * Wraps the call to CentralRepository.getInstance() throwing an exception + * if instance is null; + * + * @return Instance of CentralRepository + * + * @throws CentralRepoException + */ + private static CentralRepository getCRInstance() throws CentralRepoException { + CentralRepository instance = CentralRepository.getInstance(); + + if (instance == null) { + throw new CentralRepoException("Failed to get instance of CentralRespository, CR was null"); + } + + return instance; + } } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/RdbmsCentralRepo.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/RdbmsCentralRepo.java index 165440af45..b48797e3fc 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/RdbmsCentralRepo.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/RdbmsCentralRepo.java @@ -41,7 +41,6 @@ import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.logging.Level; -import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; @@ -53,6 +52,7 @@ import org.sleuthkit.autopsy.healthmonitor.TimingMetric; import org.sleuthkit.datamodel.Account; import org.sleuthkit.datamodel.CaseDbSchemaVersionNumber; import org.sleuthkit.datamodel.HashHitInfo; +import org.sleuthkit.datamodel.InvalidAccountIDException; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskData; @@ -1081,29 +1081,37 @@ abstract class RdbmsCentralRepo implements CentralRepository { * within TSK core */ @Override - public CentralRepoAccount getOrCreateAccount(CentralRepoAccountType crAccountType, String accountUniqueID) throws CentralRepoException { + public CentralRepoAccount getOrCreateAccount(CentralRepoAccountType crAccountType, String accountUniqueID) throws InvalidAccountIDException, CentralRepoException { // Get the account fom the accounts table - CentralRepoAccount account = getAccount(crAccountType, accountUniqueID); + String normalizedAccountID = CentralRepoAccount.normalizeAccountIdentifier(crAccountType, accountUniqueID); - // account not found in the table, create it - if (null == account) { - - String query = "INSERT INTO accounts (account_type_id, account_unique_identifier) " - + "VALUES ( " + crAccountType.getAccountTypeId() + ", '" - + accountUniqueID + "' )"; - - try (Connection connection = connect(); - Statement s = connection.createStatement();) { - - s.execute(query); - // get the account from the db - should exist now. - account = getAccount(crAccountType, accountUniqueID); - } catch (SQLException ex) { - throw new CentralRepoException("Error adding an account to CR database.", ex); - } + // insert the account. If there is a conflict, ignore it. + String insertSQL; + switch (CentralRepoDbManager.getSavedDbChoice().getDbPlatform()) { + case POSTGRESQL: + insertSQL = "INSERT INTO accounts (account_type_id, account_unique_identifier) VALUES (?, ?) " + getConflictClause(); //NON-NLS + break; + case SQLITE: + insertSQL = "INSERT OR IGNORE INTO accounts (account_type_id, account_unique_identifier) VALUES (?, ?) "; //NON-NLS + break; + default: + throw new CentralRepoException(String.format("Cannot add account to currently selected CR database platform %s", CentralRepoDbManager.getSavedDbChoice().getDbPlatform())); //NON-NLS } + - return account; + try (Connection connection = connect(); + PreparedStatement preparedStatement = connection.prepareStatement(insertSQL);) { + + preparedStatement.setInt(1, crAccountType.getAccountTypeId()); + preparedStatement.setString(2, normalizedAccountID); + + preparedStatement.executeUpdate(); + + // get the account from the db - should exist now. + return getAccount(crAccountType, normalizedAccountID); + } catch (SQLException ex) { + throw new CentralRepoException("Error adding an account to CR database.", ex); + } } @Override @@ -1183,15 +1191,17 @@ abstract class RdbmsCentralRepo implements CentralRepository { * @return CentralRepoAccount for the give type/id. May return null if not * found. * - * @throws CentralRepoException + * @throws CentralRepoException If there is an error accessing Central Repository. + * @throws InvalidAccountIDException If the account identifier is not valid. */ - private CentralRepoAccount getAccount(CentralRepoAccountType crAccountType, String accountUniqueID) throws CentralRepoException { - - CentralRepoAccount crAccount = accountsCache.getIfPresent(Pair.of(crAccountType, accountUniqueID)); + @Override + public CentralRepoAccount getAccount(CentralRepoAccountType crAccountType, String accountUniqueID) throws InvalidAccountIDException, CentralRepoException { + String normalizedAccountID = CentralRepoAccount.normalizeAccountIdentifier(crAccountType, accountUniqueID); + CentralRepoAccount crAccount = accountsCache.getIfPresent(Pair.of(crAccountType, normalizedAccountID)); if (crAccount == null) { - crAccount = getCRAccountFromDb(crAccountType, accountUniqueID); + crAccount = getCRAccountFromDb(crAccountType, normalizedAccountID); if (crAccount != null) { - accountsCache.put(Pair.of(crAccountType, accountUniqueID), crAccount); + accountsCache.put(Pair.of(crAccountType, normalizedAccountID), crAccount); } } @@ -1679,7 +1689,7 @@ abstract class RdbmsCentralRepo implements CentralRepository { bulkArtifacts.get(tableName).clear(); } - TimingMetric timingMetric = HealthMonitor.getTimingMetric("Correlation Engine: Bulk insert"); + TimingMetric timingMetric = HealthMonitor.getTimingMetric("Central Repository: Bulk insert"); HealthMonitor.submitTimingMetric(timingMetric); // Reset state @@ -2543,89 +2553,52 @@ abstract class RdbmsCentralRepo implements CentralRepository { } @Override - public void executeInsertSQL(String insertClause) throws CentralRepoException { - - if (insertClause == null) { - throw new CentralRepoException("Insert SQL is null"); - } - - String sql = getPlatformSpecificInsertSQL(insertClause); - try (Connection conn = connect(); - PreparedStatement preparedStatement = conn.prepareStatement(sql);) { + public void executeCommand(String sql, List params) throws CentralRepoException { + + try (Connection conn = connect();) { + + PreparedStatement preparedStatement = conn.prepareStatement(sql); + + // Fill in the params + if (params != null) { + int paramIndex = 1; + for (Object param : params) { + preparedStatement.setObject(paramIndex, param); + paramIndex += 1; + } + } + // execute the prepared statement preparedStatement.executeUpdate(); } catch (SQLException ex) { - throw new CentralRepoException(String.format("Error running SQL %s, exception = %s", sql, ex.getMessage()), ex); + throw new CentralRepoException(String.format("Error executing prepared statement for SQL %s", sql), ex); } } @Override - public void executeSelectSQL(String selectSQL, CentralRepositoryDbQueryCallback queryCallback) throws CentralRepoException { + public void executeQuery(String sql, List params, CentralRepositoryDbQueryCallback queryCallback) throws CentralRepoException { if (queryCallback == null) { throw new CentralRepoException("Query callback is null"); } - if (selectSQL == null) { - throw new CentralRepoException("Select SQL is null"); - } - - StringBuilder sqlSb = new StringBuilder(QUERY_STR_MAX_LEN); - if (selectSQL.trim().toUpperCase().startsWith("SELECT") == false) { - sqlSb.append("SELECT "); - } - - sqlSb.append(selectSQL); - - try (Connection conn = connect(); - PreparedStatement preparedStatement = conn.prepareStatement(sqlSb.toString()); - ResultSet resultSet = preparedStatement.executeQuery();) { - queryCallback.process(resultSet); + + try ( Connection conn = connect();) { + PreparedStatement preparedStatement = conn.prepareStatement(sql); + + // fill in the params + if (params != null) { + int paramIndex = 1; + for (Object param : params) { + preparedStatement.setObject(paramIndex, param); + paramIndex += 1; + } + } + // execute query, and the callback to process result + try (ResultSet resultSet = preparedStatement.executeQuery();) { + queryCallback.process(resultSet); + } } catch (SQLException ex) { - throw new CentralRepoException(String.format("Error running SQL %s, exception = %s", selectSQL, ex.getMessage()), ex); - } - } - - @Override - public void executeUpdateSQL(String updateSQL) throws CentralRepoException { - - if (updateSQL == null) { - throw new CentralRepoException("Update SQL is null"); - } - - StringBuilder sqlSb = new StringBuilder(QUERY_STR_MAX_LEN); - if (updateSQL.trim().toUpperCase().startsWith("UPDATE") == false) { - sqlSb.append("UPDATE "); - } - - sqlSb.append(updateSQL); - - try (Connection conn = connect(); - PreparedStatement preparedStatement = conn.prepareStatement(sqlSb.toString());) { - preparedStatement.executeUpdate(); - } catch (SQLException ex) { - throw new CentralRepoException(String.format("Error running SQL %s, exception = %s", updateSQL, ex.getMessage()), ex); - } - } - - @Override - public void executeDeleteSQL(String deleteSQL) throws CentralRepoException { - - if (deleteSQL == null) { - throw new CentralRepoException("Delete SQL is null"); - } - - StringBuilder sqlSb = new StringBuilder(QUERY_STR_MAX_LEN); - if (deleteSQL.trim().toUpperCase().startsWith("DELETE") == false) { - sqlSb.append("DELETE "); - } - - sqlSb.append(deleteSQL); - - try (Connection conn = connect(); - PreparedStatement preparedStatement = conn.prepareStatement(sqlSb.toString());) { - preparedStatement.executeUpdate(); - } catch (SQLException ex) { - throw new CentralRepoException(String.format("Error running SQL %s, exception = %s", deleteSQL, ex.getMessage()), ex); - } + throw new CentralRepoException(String.format("Error executing prepared statement for SQL query %s", sql), ex); + } } @Override diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteCentralRepo.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteCentralRepo.java index 3d2dfc09b0..df0327469d 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteCentralRepo.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteCentralRepo.java @@ -832,48 +832,27 @@ final class SqliteCentralRepo extends RdbmsCentralRepo { releaseSharedLock(); } } - + @Override - public void executeInsertSQL(String insertSQL) throws CentralRepoException { + public void executeCommand(String sql, List params) throws CentralRepoException { try { - acquireSharedLock(); - super.executeInsertSQL(insertSQL); + acquireExclusiveLock(); + super.executeCommand(sql, params); } finally { - releaseSharedLock(); + releaseExclusiveLock(); } } @Override - public void executeSelectSQL(String selectSQL, CentralRepositoryDbQueryCallback queryCallback) throws CentralRepoException { + public void executeQuery(String sql, List params, CentralRepositoryDbQueryCallback queryCallback) throws CentralRepoException { try { acquireSharedLock(); - super.executeSelectSQL(selectSQL, queryCallback); + super.executeQuery(sql, params, queryCallback); } finally { releaseSharedLock(); } } - @Override - public void executeUpdateSQL(String updateSQL) throws CentralRepoException { - try { - acquireSharedLock(); - super.executeUpdateSQL(updateSQL); - } finally { - releaseSharedLock(); - } - } - - @Override - public void executeDeleteSQL(String deleteSQL) throws CentralRepoException { - try { - acquireSharedLock(); - super.executeDeleteSQL(deleteSQL); - } finally { - releaseSharedLock(); - } - } - - /** * Check whether a reference set with the given name/version is in the * central repo. Used to check for name collisions when creating reference diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Bundle.properties-MERGED index e3c99ded13..d4cca6c407 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Bundle.properties-MERGED @@ -1,5 +1,5 @@ caseeventlistener.evidencetag=Evidence -IngestEventsListener.ingestmodule.name=Correlation Engine +IngestEventsListener.ingestmodule.name=Central Repository IngestEventsListener.prevCaseComment.text=Previous Case: # {0} - typeName # {1} - count diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/IngestEventsListener.java b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/IngestEventsListener.java index d399fe4614..03fa640ca5 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/IngestEventsListener.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/IngestEventsListener.java @@ -73,7 +73,7 @@ import org.sleuthkit.datamodel.CommunicationsUtils; * Listen for ingest events and update entries in the Central Repository * database accordingly */ -@NbBundle.Messages({"IngestEventsListener.ingestmodule.name=Correlation Engine"}) +@NbBundle.Messages({"IngestEventsListener.ingestmodule.name=Central Repository"}) public class IngestEventsListener { private static final Logger LOGGER = Logger.getLogger(CorrelationAttributeInstance.class.getName()); @@ -116,24 +116,24 @@ public class IngestEventsListener { /** * Increase the number of IngestEventsListeners adding contents to the - * Correlation Engine. + * Central Repository. */ public synchronized static void incrementCorrelationEngineModuleCount() { - correlationModuleInstanceCount++; //Should be called once in the Correlation Engine module's startup method. + correlationModuleInstanceCount++; //Should be called once in the Central Repository module's startup method. } /** * Decrease the number of IngestEventsListeners adding contents to the - * Correlation Engine. + * Central Repository. */ public synchronized static void decrementCorrelationEngineModuleCount() { if (getCeModuleInstanceCount() > 0) { //prevent it ingestJobCounter from going negative - correlationModuleInstanceCount--; //Should be called once in the Correlation Engine module's shutdown method. + correlationModuleInstanceCount--; //Should be called once in the Central Repository module's shutdown method. } } /** - * Reset the counter which keeps track of if the Correlation Engine Module + * Reset the counter which keeps track of if the Central Repository Module * is being run during injest to 0. */ synchronized static void resetCeModuleInstanceCount() { @@ -141,10 +141,10 @@ public class IngestEventsListener { } /** - * Whether or not the Correlation Engine Module is enabled for any of the + * Whether or not the Central Repository Module is enabled for any of the * currently running ingest jobs. * - * @return boolean True for Correlation Engine enabled, False for disabled + * @return boolean True for Central Repository enabled, False for disabled */ public synchronized static int getCeModuleInstanceCount() { return correlationModuleInstanceCount; @@ -282,7 +282,7 @@ public class IngestEventsListener { @Override public void propertyChange(PropertyChangeEvent evt) { - //if ingest is running we want there to check if there is a Correlation Engine module running + //if ingest is running we want there to check if there is a Central Repository module running //sometimes artifacts are generated by DSPs or other sources while ingest is not running //in these cases we still want to create correlation attributesForNewArtifact for those artifacts when appropriate if (!IngestManager.getInstance().isIngestRunning() || getCeModuleInstanceCount() > 0) { @@ -349,7 +349,7 @@ public class IngestEventsListener { if (getCeModuleInstanceCount() == 0) { recentlyAddedCeArtifacts.clear(); } - //else another instance of the Correlation Engine Module is still being run. + //else another instance of the Central Repository Module is still being run. /* * Ensure the data source in the Central Repository has hash values diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/Bundle.properties-MERGED index 96a73954fa..46a2f01a64 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/Bundle.properties-MERGED @@ -1,6 +1,6 @@ CentralRepoIngestModel_name_header=Name:
CentralRepoIngestModel_previous_case_header=
Previous Cases:
-CentralRepoIngestModule.errorMessage.isNotEnabled=Central repository settings are not initialized, cannot run Correlation Engine ingest module. +CentralRepoIngestModule.errorMessage.isNotEnabled=Central repository settings are not initialized, cannot run Central Repository ingest module. CentralRepoIngestModule.notfyBubble.title=Central Repository Not Initialized CentralRepoIngestModule.prevCaseComment.text=Previous Case: CentralRepoIngestModule.prevTaggedSet.text=Previously Tagged As Notable (Central Repository) @@ -8,7 +8,7 @@ CentralRepoIngestModule_notable_message_header=A file in this data source # {0} - Name of file that is Notable CentralRepoIngestModule_postToBB_knownBadMsg=Notable: {0} CentralRepoIngestModuleFactory.ingestmodule.desc=Saves properties to the central repository for later correlation -CentralRepoIngestModuleFactory.ingestmodule.name=Correlation Engine +CentralRepoIngestModuleFactory.ingestmodule.name=Central Repository IngestSettingsPanel.ingestSettingsLabel.text=Ingest Settings IngestSettingsPanel.flagTaggedNotableItemsCheckbox.text=Flag items previously tagged as notable IngestSettingsPanel.flagPreviouslySeenDevicesCheckbox.text=Flag devices previously seen in other cases diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/CentralRepoIngestModule.java b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/CentralRepoIngestModule.java index 1f7472d330..a7c3bd41e0 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/CentralRepoIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/CentralRepoIngestModule.java @@ -85,7 +85,7 @@ final class CentralRepoIngestModule implements FileIngestModule { private final boolean createCorrelationProperties; /** - * Instantiate the Correlation Engine ingest module. + * Instantiate the Central Repository ingest module. * * @param settings The ingest settings for the module instance. */ @@ -147,7 +147,7 @@ final class CentralRepoIngestModule implements FileIngestModule { */ if (abstractFile.getKnown() != TskData.FileKnown.KNOWN && flagTaggedNotableItems) { try { - TimingMetric timingMetric = HealthMonitor.getTimingMetric("Correlation Engine: Notable artifact query"); + TimingMetric timingMetric = HealthMonitor.getTimingMetric("Central Repository: Notable artifact query"); List caseDisplayNamesList = dbManager.getListCasesHavingArtifactInstancesKnownBad(filesType, md5); HealthMonitor.submitTimingMetric(timingMetric); if (!caseDisplayNamesList.isEmpty()) { @@ -220,7 +220,7 @@ final class CentralRepoIngestModule implements FileIngestModule { // see ArtifactManagerTimeTester for details @Messages({ "CentralRepoIngestModule.notfyBubble.title=Central Repository Not Initialized", - "CentralRepoIngestModule.errorMessage.isNotEnabled=Central repository settings are not initialized, cannot run Correlation Engine ingest module." + "CentralRepoIngestModule.errorMessage.isNotEnabled=Central repository settings are not initialized, cannot run Central Repository ingest module." }) @Override public void startUp(IngestJobContext context) throws IngestModuleException { @@ -235,7 +235,7 @@ final class CentralRepoIngestModule implements FileIngestModule { * posited. * * Note: Flagging cannot be disabled if any other instances of the - * Correlation Engine module are running. This restriction is to prevent + * Central Repository module are running. This restriction is to prevent * missing results in the case where the first module is flagging * notable items, and the proceeding module (with flagging disabled) * causes the first to stop flagging. @@ -276,7 +276,7 @@ final class CentralRepoIngestModule implements FileIngestModule { // Don't allow sqlite central repo databases to be used for multi user cases if ((autopsyCase.getCaseType() == Case.CaseType.MULTI_USER_CASE) && (CentralRepoDbManager.getSavedDbChoice().getDbPlatform() == CentralRepoPlatforms.SQLITE)) { - logger.log(Level.SEVERE, "Cannot run correlation engine on a multi-user case with a SQLite central repository."); + logger.log(Level.SEVERE, "Cannot run Central Repository ingest module on a multi-user case with a SQLite central repository."); throw new IngestModuleException("Cannot run on a multi-user case with a SQLite central repository."); // NON-NLS } jobId = context.getJobId(); diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/CentralRepoIngestModuleFactory.java b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/CentralRepoIngestModuleFactory.java index 078c3a5ac9..1c34f1ffad 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/CentralRepoIngestModuleFactory.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/CentralRepoIngestModuleFactory.java @@ -33,7 +33,7 @@ import org.sleuthkit.autopsy.ingest.NoIngestModuleIngestJobSettings; * Factory for Central Repository ingest modules */ @ServiceProvider(service = org.sleuthkit.autopsy.ingest.IngestModuleFactory.class) -@NbBundle.Messages({"CentralRepoIngestModuleFactory.ingestmodule.name=Correlation Engine", +@NbBundle.Messages({"CentralRepoIngestModuleFactory.ingestmodule.name=Central Repository", "CentralRepoIngestModuleFactory.ingestmodule.desc=Saves properties to the central repository for later correlation"}) public class CentralRepoIngestModuleFactory extends IngestModuleFactoryAdapter { diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettings.java b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettings.java index c48f2811a0..52d645bcac 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettings.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettings.java @@ -21,7 +21,7 @@ package org.sleuthkit.autopsy.centralrepository.ingestmodule; import org.sleuthkit.autopsy.ingest.IngestModuleIngestJobSettings; /** - * Ingest job settings for the Correlation Engine module. + * Ingest job settings for the Central Repository module. */ final class IngestSettings implements IngestModuleIngestJobSettings { diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.java index 29e2d91253..befe405281 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.java @@ -22,7 +22,7 @@ import org.sleuthkit.autopsy.ingest.IngestModuleIngestJobSettings; import org.sleuthkit.autopsy.ingest.IngestModuleIngestJobSettingsPanel; /** - * Ingest job settings panel for the Correlation Engine module. + * Ingest job settings panel for the Central Repository module. */ @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives final class IngestSettingsPanel extends IngestModuleIngestJobSettingsPanel { diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.java b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.java index 675e7c8807..f30c402513 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.java @@ -43,6 +43,7 @@ import javax.swing.event.DocumentListener; import javax.swing.filechooser.FileFilter; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; +import org.openide.windows.TopComponent; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbChoice; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbManager; @@ -660,6 +661,8 @@ public class EamDbSettingsDialog extends JDialog { * found. */ private static boolean testStatusAndCreate(Component parent, CentralRepoDbManager manager, EamDbSettingsDialog dialog) { + closePersonasTopComponent(); + parent.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); manager.testStatus(); @@ -690,6 +693,21 @@ public class EamDbSettingsDialog extends JDialog { parent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); return true; } + + + + /** + * Closes Personas top component if it exists. + */ + private static void closePersonasTopComponent() { + SwingUtilities.invokeLater(() -> { + TopComponent personasWindow = WindowManager.getDefault().findTopComponent("PersonasTopComponent"); + if (personasWindow != null && personasWindow.isOpened()) { + personasWindow.close(); + } + }); + } + /** * This method returns if changes to the central repository configuration diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/Bundle.properties b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/Bundle.properties index 24fdf1c0ff..5a218ba942 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/Bundle.properties @@ -1,21 +1,8 @@ -CTL_OpenPersonaManager=Persona Manager -CTL_PersonaManagerTopComponentAction=Persona Manager +CTL_OpenPersonas=Personas +CTL_PersonasTopComponentAction=Personas CTL_PersonaDetailsTopComponent=Persona Details -PersonaManagerTopComponent.createBtn.text=New Persona -PersonaManagerTopComponent.searchBtn.text=Search -PersonaManagerTopComponent.resultsTable.columnModel.title1=Name -PersonaManagerTopComponent.resultsTable.columnModel.title0=ID -PersonaManagerTopComponent.resultsTable.toolTipText= -PersonaManagerTopComponent.searchAccountRadio.text=Account -PersonaManagerTopComponent.searchNameRadio.text=Name -PersonaManagerTopComponent.searchField.text= -AddAccountDialog.cancelBtn.text=Cancel -AddAccountDialog.okBtn.text=OK -PersonaManagerTopComponent.editBtn.text=Edit Persona PersonaDetailsDialog.cancelBtn.text=Cancel PersonaDetailsDialog.okBtn.text=OK -PersonaDetailsPanel.deleteCaseBtn.text=Delete -PersonaDetailsPanel.addCaseBtn.text=Add PersonaDetailsPanel.casesLbl.text=Cases found in: PersonaDetailsPanel.deleteAliasBtn.text=Delete PersonaDetailsPanel.addAliasBtn.text=Add @@ -31,26 +18,66 @@ PersonaDetailsPanel.nameLbl.text=Name: AddAliasDialog.accountsLbl.text=Account: AddAliasDialog.okBtn.text=OK AddAliasDialog.cancelBtn.text=Cancel -AddMetadataDialog.cancelBtn.text=Cancel -AddMetadataDialog.okBtn.text=OK -AddMetadataDialog.nameLbl.text=Name: -AddMetadataDialog.nameTextField.text= -AddMetadataDialog.valueLbl.text=Value: -AddMetadataDialog.valueTextField.text= -AddMetadataDialog.justificationLbl.text=Justification: -AddMetadataDialog.justificationTextField.text= -AddMetadataDialog.confidenceLbl.text=Confidence: -AddAliasDialog.justificationLbl.text=Justification: -AddAliasDialog.okBtn.text_1=OK -AddAliasDialog.cancelBtn.text_1=Cancel -AddAliasDialog.confidenceLbl.text=Confidence: -AddAliasDialog.justificationTextField.text= -AddAliasDialog.aliasLbl.text=Alias: -AddAliasDialog.aliasTextField.text= -AddAccountDialog.justificationTextField.text= -AddAccountDialog.justificationLbl.text=Justification: -AddAccountDialog.confidenceLbl.text=Confidence: -AddAccountDialog.typeLbl.text=Type: -AddAccountDialog.identiferLbl.text=Identifier: -AddAccountDialog.identifierTextField.text= -PersonaManagerTopComponent.deleteBtn.text=Delete Persona +PersonaDetailsPanel.casesLbl.text=Cases found in: +PersonaDetailsPanel.deleteAliasBtn.text=Delete +PersonaDetailsPanel.addAliasBtn.text=Add +PersonaDetailsPanel.aliasesLabel.text=Aliases: +PersonaDetailsPanel.deleteMetadataBtn.text=Delete +PersonaDetailsPanel.addMetadataBtn.text=Add +PersonaDetailsPanel.metadataLabel.text=Metadata: +PersonaDetailsPanel.deleteAccountBtn.text=Delete +PersonaDetailsPanel.addAccountBtn.text=Add +PersonaDetailsPanel.accountsLbl.text=Accounts: +PersonaDetailsPanel.commentField.text= +PersonaDetailsPanel.commentLbl.text=Comment: +PersonaDetailsPanel.nameField.text= +PersonaDetailsPanel.nameLbl.text=Name: +PersonaDetailsPanel.examinerLbl.text=Created by: +PersonaDetailsPanel.examinerField.text= +PersonaDetailsPanel.creationDateLbl.text=Created on: +PersonaDetailsPanel.creationDateField.text= +PersonaAccountDialog.confidenceLbl.text=Confidence: +PersonaAccountDialog.justificationTextField.text= +PersonaAccountDialog.justificationLbl.text=Justification: +PersonaAccountDialog.typeLbl.text=Type: +PersonaAccountDialog.identifierTextField.text= +PersonaAccountDialog.identiferLbl.text=Identifier: +PersonaAccountDialog.okBtn.text=OK +PersonaAccountDialog.cancelBtn.text=Cancel +PersonaAliasDialog.cancelBtn.text_1=Cancel +PersonaAliasDialog.confidenceLbl.text=Confidence: +PersonaAliasDialog.justificationTextField.text= +PersonaAliasDialog.justificationLbl.text=Justification: +PersonaAliasDialog.aliasTextField.text= +PersonaAliasDialog.aliasLbl.text=Alias: +PersonaAliasDialog.okBtn.text_1=OK +PersonaMetadataDialog.confidenceLbl.text=Confidence: +PersonaMetadataDialog.justificationTextField.text= +PersonaMetadataDialog.justificationLbl.text=Justification: +PersonaMetadataDialog.valueTextField.text= +PersonaMetadataDialog.valueLbl.text=Value: +PersonaMetadataDialog.nameTextField.text= +PersonaMetadataDialog.nameLbl.text=Name: +PersonaMetadataDialog.okBtn.text=OK +PersonaMetadataDialog.cancelBtn.text=Cancel +PersonaDetailsPanel.editAccountBtn.text=Edit +PersonaDetailsPanel.editMetadataBtn.text=Edit +PersonaDetailsPanel.editAliasBtn.text=Edit +PersonasTopComponent.searchAccountRadio.text=Account +PersonasTopComponent.searchNameRadio.text=Name +PersonasTopComponent.searchField.text= +PersonasTopComponent.deleteBtn.text=Delete Persona +PersonasTopComponent.editBtn.text=Edit Persona +PersonasTopComponent.createBtn.text=New Persona +PersonasTopComponent.createAccountBtn.text=Create Account +PersonasTopComponent.searchBtn.text=Show +PersonasTopComponent.resultsTable.columnModel.title1=Name +PersonasTopComponent.resultsTable.columnModel.title0=ID +PersonasTopComponent.resultsTable.toolTipText= +CreatePersonaAccountDialog.cancelBtn.text=Cancel +CreatePersonaAccountDialog.typeLbl.text=Type: +CreatePersonaAccountDialog.identifierTextField.text= +CreatePersonaAccountDialog.identiferLbl.text=Identifier: +CreatePersonaAccountDialog.okBtn.text=OK +PersonasTopComponent.introText.text=Personas represent an online identity. They span cases and are stored in the Central Repository based on accounts that were found in artifacts. You can create, edit, and delete personas here. +PersonasTopComponent.cbFilterByKeyword.text=Filter personas by keyword diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/Bundle.properties-MERGED index 3c7765f09c..e8a7f9718a 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/Bundle.properties-MERGED @@ -1,52 +1,42 @@ -AddAccountDialog.title.text=Add Account -AddAccountDialog_dup_msg=This account is already added to the persona -AddAccountDialog_dup_Title=Account add failure -AddAccountDialog_empty_msg=The identifier field cannot be empty -AddAccountDialog_empty_Title=Empty identifier -AddAccountDialog_get_types_exception_msg=Failed to access central repository -AddAccountDialog_get_types_exception_Title=Central Repository failure -AddAccountDialog_search_empty_msg=Account not found for given identifier and type -AddAccountDialog_search_empty_Title=Account not found -AddAccountDialog_search_failure_msg=Central Repository account search failed -AddAccountDialog_search_failure_Title=Account add failure -AddAliasDialog.title.text=Add Alias -AddAliasDialog_dup_msg=This alias has already been added to this persona -AddAliasDialog_dup_Title=Alias add failure AddMetadataDialog.title.text=Add Metadata -AddMetadataDialog_dup_msg=A metadata entry with this name has already been added to this persona +AddMetadataDialog_dup_msg=A metadata entry with this name has already been added to this persona. AddMetadataDialog_dup_Title=Metadata add failure -CTL_OpenPersonaManager=Persona Manager -CTL_PersonaManagerTopComponentAction=Persona Manager +AddMetadataDialog_empty_name_msg=A metadata entry cannot have an empty name or value. +AddMetadataDialog_empty_name_Title=Missing field(s) +CreatePersonaAccountDialog.title.text=Create Account +CreatePersonaAccountDialog_error_msg=Failed to create account. +CreatePersonaAccountDialog_error_title=Account failure +CreatePersonaAccountDialog_invalid_account_msg=Account identifier is not valid. +CreatePersonaAccountDialog_invalid_account_Title=Invalid account identifier +CreatePersonaAccountDialog_success_msg=Account added. +CreatePersonaAccountDialog_success_title=Account added +CTL_OpenPersonas=Personas +CTL_PersonasTopComponentAction=Personas CTL_PersonaDetailsTopComponent=Persona Details -OpenPersonasAction.displayName=Persona Manager -PersonaDetailsDialogCreateTitle=Create Persona -PersonaDetailsDialogEditTitle=Edit Persona -PersonaDetailsPanel_CentralRepoErr_msg=Failure to write to Central Repository -PersonaDetailsPanel_CentralRepoErr_Title=Central Repository failure -PersonaDetailsPanel_EmptyName_msg=Persona name cannot be empty -PersonaDetailsPanel_EmptyName_Title=Empty persona name -PersonaDetailsPanel_load_exception_msg=Failed to load persona -PersonaDetailsPanel_load_exception_Title=Initialization failure -PersonaDetailsPanel_NameCreate=Create Persona -PersonaDetailsPanel_NameEdit=Edit Persona -PersonaDetailsPanel_NameView=View Persona -PersonaDetailsPanel_NotEnoughAccounts_msg=Two or more accounts are necessary to create a persona -PersonaDetailsPanel_NotEnoughAccounts_Title=Not enough accounts -PersonaManagerTopComponent.createBtn.text=New Persona -PersonaManagerTopComponent.searchBtn.text=Search -PersonaManagerTopComponent.resultsTable.columnModel.title1=Name -PersonaManagerTopComponent.resultsTable.columnModel.title0=ID -PersonaManagerTopComponent.resultsTable.toolTipText= -PersonaManagerTopComponent.searchAccountRadio.text=Account -PersonaManagerTopComponent.searchNameRadio.text=Name -PersonaManagerTopComponent.searchField.text= -AddAccountDialog.cancelBtn.text=Cancel -AddAccountDialog.okBtn.text=OK -PersonaManagerTopComponent.editBtn.text=Edit Persona +OpenPersonasAction.displayName=Personas +PersonaAccountDialog.title.text=Add Account +PersonaAccountDialog_dup_msg=This account is already added to the persona. +PersonaAccountDialog_dup_Title=Account add failure +PersonaAccountDialog_get_types_exception_msg=Failed to access central repository. +PersonaAccountDialog_get_types_exception_Title=Central Repository failure +PersonaAccountDialog_identifier_empty_msg=The identifier field cannot be empty. +PersonaAccountDialog_identifier_empty_Title=Empty identifier +PersonaAccountDialog_invalid_account_msg=Account identifier is not valid. +PersonaAccountDialog_invalid_account_Title=Invalid account identifier +PersonaAccountDialog_search_empty_msg=Account not found for given identifier and type. +PersonaAccountDialog_search_empty_Title=Account not found +PersonaAccountDialog_search_failure_msg=Central Repository account search failed. +PersonaAccountDialog_search_failure_Title=Account add failure +PersonaAliasDialog.title.text=Add Alias +PersonaAliasDialog_dup_msg=This alias has already been added to this persona. +PersonaAliasDialog_dup_Title=Alias add failure +PersonaAliasDialog_empty_msg=An alias cannot be empty. +PersonaAliasDialog_empty_Title=Empty alias PersonaDetailsDialog.cancelBtn.text=Cancel PersonaDetailsDialog.okBtn.text=OK -PersonaDetailsPanel.deleteCaseBtn.text=Delete -PersonaDetailsPanel.addCaseBtn.text=Add +PersonaDetailsDialogCreateTitle=Create Persona +PersonaDetailsDialogEditTitle=Edit Persona +PersonaDetailsDialogViewTitle=View Persona PersonaDetailsPanel.casesLbl.text=Cases found in: PersonaDetailsPanel.deleteAliasBtn.text=Delete PersonaDetailsPanel.addAliasBtn.text=Add @@ -62,28 +52,86 @@ PersonaDetailsPanel.nameLbl.text=Name: AddAliasDialog.accountsLbl.text=Account: AddAliasDialog.okBtn.text=OK AddAliasDialog.cancelBtn.text=Cancel -AddMetadataDialog.cancelBtn.text=Cancel -AddMetadataDialog.okBtn.text=OK -AddMetadataDialog.nameLbl.text=Name: -AddMetadataDialog.nameTextField.text= -AddMetadataDialog.valueLbl.text=Value: -AddMetadataDialog.valueTextField.text= -AddMetadataDialog.justificationLbl.text=Justification: -AddMetadataDialog.justificationTextField.text= -AddMetadataDialog.confidenceLbl.text=Confidence: -AddAliasDialog.justificationLbl.text=Justification: -AddAliasDialog.okBtn.text_1=OK -AddAliasDialog.cancelBtn.text_1=Cancel -AddAliasDialog.confidenceLbl.text=Confidence: -AddAliasDialog.justificationTextField.text= -AddAliasDialog.aliasLbl.text=Alias: -AddAliasDialog.aliasTextField.text= -AddAccountDialog.justificationTextField.text= -AddAccountDialog.justificationLbl.text=Justification: -AddAccountDialog.confidenceLbl.text=Confidence: -AddAccountDialog.typeLbl.text=Type: -AddAccountDialog.identiferLbl.text=Identifier: -AddAccountDialog.identifierTextField.text= -PMTopComponent_Name=Persona Manager -PMTopComponent_search_exception_msg=Failed to search personas -PMTopComponent_search_exception_Title=Search failure +PersonaDetailsPanel.casesLbl.text=Cases found in: +PersonaDetailsPanel.deleteAliasBtn.text=Delete +PersonaDetailsPanel.addAliasBtn.text=Add +PersonaDetailsPanel.aliasesLabel.text=Aliases: +PersonaDetailsPanel.deleteMetadataBtn.text=Delete +PersonaDetailsPanel.addMetadataBtn.text=Add +PersonaDetailsPanel.metadataLabel.text=Metadata: +PersonaDetailsPanel.deleteAccountBtn.text=Delete +PersonaDetailsPanel.addAccountBtn.text=Add +PersonaDetailsPanel.accountsLbl.text=Accounts: +PersonaDetailsPanel.commentField.text= +PersonaDetailsPanel.commentLbl.text=Comment: +PersonaDetailsPanel.nameField.text= +PersonaDetailsPanel.nameLbl.text=Name: +PersonaDetailsPanel.examinerLbl.text=Created by: +PersonaDetailsPanel.examinerField.text= +PersonaDetailsPanel.creationDateLbl.text=Created on: +PersonaDetailsPanel.creationDateField.text= +PersonaAccountDialog.confidenceLbl.text=Confidence: +PersonaAccountDialog.justificationTextField.text= +PersonaAccountDialog.justificationLbl.text=Justification: +PersonaAccountDialog.typeLbl.text=Type: +PersonaAccountDialog.identifierTextField.text= +PersonaAccountDialog.identiferLbl.text=Identifier: +PersonaAccountDialog.okBtn.text=OK +PersonaAccountDialog.cancelBtn.text=Cancel +PersonaAliasDialog.cancelBtn.text_1=Cancel +PersonaAliasDialog.confidenceLbl.text=Confidence: +PersonaAliasDialog.justificationTextField.text= +PersonaAliasDialog.justificationLbl.text=Justification: +PersonaAliasDialog.aliasTextField.text= +PersonaAliasDialog.aliasLbl.text=Alias: +PersonaAliasDialog.okBtn.text_1=OK +PersonaDetailsPanel_CentralRepoErr_msg=Failure to write to Central Repository. +PersonaDetailsPanel_CentralRepoErr_Title=Central Repository failure +PersonaDetailsPanel_empty_justification_msg=The justification field cannot be empty +PersonaDetailsPanel_empty_justification_Title=Empty justification +PersonaDetailsPanel_EmptyComment_msg=Persona comment cannot be empty. +PersonaDetailsPanel_EmptyComment_Title=Empty persona comment +PersonaDetailsPanel_EmptyName_msg=Persona name cannot be empty. +PersonaDetailsPanel_EmptyName_Title=Empty persona name +PersonaDetailsPanel_load_exception_msg=Failed to load persona. +PersonaDetailsPanel_load_exception_Title=Initialization failure +PersonaDetailsPanel_NotEnoughAccounts_msg=A persona needs at least one account. +PersonaDetailsPanel_NotEnoughAccounts_Title=Missing account +PersonaMetadataDialog.confidenceLbl.text=Confidence: +PersonaMetadataDialog.justificationTextField.text= +PersonaMetadataDialog.justificationLbl.text=Justification: +PersonaMetadataDialog.valueTextField.text= +PersonaMetadataDialog.valueLbl.text=Value: +PersonaMetadataDialog.nameTextField.text= +PersonaMetadataDialog.nameLbl.text=Name: +PersonaMetadataDialog.okBtn.text=OK +PersonaMetadataDialog.cancelBtn.text=Cancel +PersonaDetailsPanel.editAccountBtn.text=Edit +PersonaDetailsPanel.editMetadataBtn.text=Edit +PersonaDetailsPanel.editAliasBtn.text=Edit +PersonasTopComponent.searchAccountRadio.text=Account +PersonasTopComponent.searchNameRadio.text=Name +PersonasTopComponent.searchField.text= +PersonasTopComponent.deleteBtn.text=Delete Persona +PersonasTopComponent.editBtn.text=Edit Persona +PersonasTopComponent.createBtn.text=New Persona +PersonasTopComponent.createAccountBtn.text=Create Account +PersonasTopComponent.searchBtn.text=Show +PersonasTopComponent.resultsTable.columnModel.title1=Name +PersonasTopComponent.resultsTable.columnModel.title0=ID +PersonasTopComponent.resultsTable.toolTipText= +CreatePersonaAccountDialog.cancelBtn.text=Cancel +CreatePersonaAccountDialog.typeLbl.text=Type: +CreatePersonaAccountDialog.identifierTextField.text= +CreatePersonaAccountDialog.identiferLbl.text=Identifier: +CreatePersonaAccountDialog.okBtn.text=OK +PersonasTopComponent.introText.text=Personas represent an online identity. They span cases and are stored in the Central Repository based on accounts that were found in artifacts. You can create, edit, and delete personas here. +PersonasTopComponent.cbFilterByKeyword.text=Filter personas by keyword +PersonasTopComponent_delete_confirmation_msg=Are you sure you want to delete this persona? +PersonasTopComponent_delete_confirmation_Title=Are you sure? +PersonasTopComponent_delete_exception_msg=Failed to delete persona. +PersonasTopComponent_delete_exception_Title=Delete failure +PersonasTopComponent_Name=Personas +PersonasTopComponent_noCR_msg=Central Repository is not enabled. +PersonasTopComponent_search_exception_msg=Failed to search personas. +PersonasTopComponent_search_exception_Title=There was a failure during the search. Try opening a case to fully initialize the central repository database. diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/Bundle_ja.properties index 224ac7c947..bfdd721a69 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/Bundle_ja.properties @@ -1,10 +1,12 @@ -AddAccountDialog.cancelBtn.text=\u53d6\u308a\u6d88\u3057 -AddAccountDialog.okBtn.text=OK PersonaDetailsDialog.okBtn.text=OK PersonaDetailsDialog.cancelBtn.text=\u53d6\u308a\u6d88\u3057 AddAliasDialog.cancelBtn.text=\u53d6\u308a\u6d88\u3057 AddAliasDialog.okBtn.text=OK -AddMetadataDialog.okBtn.text=OK -AddMetadataDialog.cancelBtn.text=\u53d6\u308a\u6d88\u3057 -AddAliasDialog.cancelBtn.text_1=\u53d6\u308a\u6d88\u3057 -AddAliasDialog.okBtn.text_1=OK +PersonaAccountDialog.okBtn.text=OK +PersonaAccountDialog.cancelBtn.text=\u53d6\u308a\u6d88\u3057 +PersonaAliasDialog.cancelBtn.text_1=\u53d6\u308a\u6d88\u3057 +PersonaAliasDialog.okBtn.text_1=OK +PersonaMetadataDialog.okBtn.text=OK +PersonaMetadataDialog.cancelBtn.text=\u53d6\u308a\u6d88\u3057 +CreatePersonaAccountDialog.okBtn.text=OK +CreatePersonaAccountDialog.cancelBtn.text=\u53d6\u308a\u6d88\u3057 diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/CreatePersonaAccountDialog.form b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/CreatePersonaAccountDialog.form new file mode 100644 index 0000000000..fd74cca117 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/CreatePersonaAccountDialog.form @@ -0,0 +1,168 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/CreatePersonaAccountDialog.java b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/CreatePersonaAccountDialog.java new file mode 100644 index 0000000000..cfdf990710 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/CreatePersonaAccountDialog.java @@ -0,0 +1,290 @@ +/* + * Central Repository + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit 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.centralrepository.persona; + +import java.awt.Component; +import java.io.Serializable; +import java.util.Collection; +import java.util.logging.Level; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.ListCellRenderer; +import javax.swing.SwingUtilities; +import org.apache.commons.lang3.StringUtils; +import org.openide.util.NbBundle.Messages; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoAccount; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoAccount.CentralRepoAccountType; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.InvalidAccountIDException; + +/** + * Configuration dialog for creating an account. + */ +@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives +public class CreatePersonaAccountDialog extends JDialog { + + private static final Logger logger = Logger.getLogger(CreatePersonaAccountDialog.class.getName()); + + private static final long serialVersionUID = 1L; + + private final TypeChoiceRenderer TYPE_CHOICE_RENDERER = new TypeChoiceRenderer(); + + /** + * Creates new create account dialog. + */ + @Messages({"CreatePersonaAccountDialog.title.text=Create Account",}) + public CreatePersonaAccountDialog(PersonaDetailsPanel pdp) { + super(SwingUtilities.windowForComponent(pdp), + Bundle.PersonaAccountDialog_title_text(), + ModalityType.APPLICATION_MODAL); + + initComponents(); + typeComboBox.setRenderer(TYPE_CHOICE_RENDERER); + display(); + } + + /** + * This class handles displaying and rendering drop down menu for account + * choices. + */ + private class TypeChoiceRenderer extends JLabel implements ListCellRenderer, Serializable { + + private static final long serialVersionUID = 1L; + + @Override + public Component getListCellRendererComponent( + JList list, CentralRepoAccountType value, + int index, boolean isSelected, boolean cellHasFocus) { + setText(value.getAcctType().getDisplayName()); + return this; + } + } + + private CentralRepoAccountType[] getAllAccountTypes() { + Collection allAccountTypes; + try { + allAccountTypes = CentralRepository.getInstance().getAllAccountTypes(); + } catch (CentralRepoException e) { + logger.log(Level.SEVERE, "Failed to access central repository", e); + JOptionPane.showMessageDialog(this, + Bundle.PersonaAccountDialog_get_types_exception_Title(), + Bundle.PersonaAccountDialog_get_types_exception_msg(), + JOptionPane.ERROR_MESSAGE); + return new CentralRepoAccountType[0]; + } + return allAccountTypes.toArray(new CentralRepoAccountType[0]); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + settingsPanel = new javax.swing.JPanel(); + identiferLbl = new javax.swing.JLabel(); + identifierTextField = new javax.swing.JTextField(); + typeLbl = new javax.swing.JLabel(); + typeComboBox = new javax.swing.JComboBox<>(); + cancelBtn = new javax.swing.JButton(); + okBtn = new javax.swing.JButton(); + + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + setResizable(false); + + settingsPanel.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + + org.openide.awt.Mnemonics.setLocalizedText(identiferLbl, org.openide.util.NbBundle.getMessage(CreatePersonaAccountDialog.class, "CreatePersonaAccountDialog.identiferLbl.text")); // NOI18N + + identifierTextField.setText(org.openide.util.NbBundle.getMessage(CreatePersonaAccountDialog.class, "CreatePersonaAccountDialog.identifierTextField.text")); // NOI18N + identifierTextField.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + identifierTextFieldActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(typeLbl, org.openide.util.NbBundle.getMessage(CreatePersonaAccountDialog.class, "CreatePersonaAccountDialog.typeLbl.text")); // NOI18N + + typeComboBox.setModel(new javax.swing.DefaultComboBoxModel<>(getAllAccountTypes())); + + javax.swing.GroupLayout settingsPanelLayout = new javax.swing.GroupLayout(settingsPanel); + settingsPanel.setLayout(settingsPanelLayout); + settingsPanelLayout.setHorizontalGroup( + settingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(settingsPanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(settingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(settingsPanelLayout.createSequentialGroup() + .addComponent(typeLbl) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(typeComboBox, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(settingsPanelLayout.createSequentialGroup() + .addComponent(identiferLbl) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(identifierTextField, javax.swing.GroupLayout.DEFAULT_SIZE, 281, Short.MAX_VALUE))) + .addContainerGap()) + ); + settingsPanelLayout.setVerticalGroup( + settingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(settingsPanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(settingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(identiferLbl) + .addComponent(identifierTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(settingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(typeComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(typeLbl, javax.swing.GroupLayout.PREFERRED_SIZE, 9, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + + org.openide.awt.Mnemonics.setLocalizedText(cancelBtn, org.openide.util.NbBundle.getMessage(CreatePersonaAccountDialog.class, "CreatePersonaAccountDialog.cancelBtn.text")); // NOI18N + cancelBtn.setMaximumSize(new java.awt.Dimension(79, 23)); + cancelBtn.setMinimumSize(new java.awt.Dimension(79, 23)); + cancelBtn.setPreferredSize(new java.awt.Dimension(79, 23)); + cancelBtn.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cancelBtnActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(okBtn, org.openide.util.NbBundle.getMessage(CreatePersonaAccountDialog.class, "CreatePersonaAccountDialog.okBtn.text")); // NOI18N + okBtn.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + okBtnActionPerformed(evt); + } + }); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap(194, Short.MAX_VALUE) + .addComponent(okBtn) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cancelBtn, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + .addComponent(settingsPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + ); + + layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {cancelBtn, okBtn}); + + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(settingsPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(okBtn) + .addComponent(cancelBtn, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + + pack(); + }// //GEN-END:initComponents + + private void display() { + this.setLocationRelativeTo(WindowManager.getDefault().getMainWindow()); + setVisible(true); + } + + @Messages({ + "CreatePersonaAccountDialog_error_title=Account failure", + "CreatePersonaAccountDialog_error_msg=Failed to create account.", + "CreatePersonaAccountDialog_invalid_account_Title=Invalid account identifier", + "CreatePersonaAccountDialog_invalid_account_msg=Account identifier is not valid.",}) + private CentralRepoAccount createAccount(CentralRepoAccount.CentralRepoAccountType type, String identifier) { + CentralRepoAccount ret = null; + try { + CentralRepository cr = CentralRepository.getInstance(); + if (cr != null) { + ret = cr.getOrCreateAccount(type, identifier); + } + } catch (CentralRepoException e) { + logger.log(Level.SEVERE, "Failed to create account", e); + JOptionPane.showMessageDialog(this, + Bundle.CreatePersonaAccountDialog_error_msg(), + Bundle.CreatePersonaAccountDialog_error_title(), + JOptionPane.ERROR_MESSAGE); + } catch (InvalidAccountIDException e) { + logger.log(Level.WARNING, "Invalid account identifier", e); + JOptionPane.showMessageDialog(this, + Bundle.CreatePersonaAccountDialog_invalid_account_msg(), + Bundle.CreatePersonaAccountDialog_invalid_account_Title(), + JOptionPane.ERROR_MESSAGE); + } + return ret; + } + + @Messages({ + "CreatePersonaAccountDialog_success_title=Account added", + "CreatePersonaAccountDialog_success_msg=Account added.", + }) + private void okBtnActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okBtnActionPerformed + if (StringUtils.isBlank(identifierTextField.getText())) { + JOptionPane.showMessageDialog(this, + Bundle.PersonaAccountDialog_identifier_empty_msg(), + Bundle.PersonaAccountDialog_identifier_empty_Title(), + JOptionPane.ERROR_MESSAGE); + return; + } + + CentralRepoAccount.CentralRepoAccountType type = + (CentralRepoAccount.CentralRepoAccountType) typeComboBox.getSelectedItem(); + String identifier = identifierTextField.getText(); + + if (createAccount(type, identifier) != null) { + // show account created message + JOptionPane.showMessageDialog(this, + Bundle.CreatePersonaAccountDialog_success_msg(), + Bundle.CreatePersonaAccountDialog_success_title(), + JOptionPane.INFORMATION_MESSAGE); + + dispose(); + } + }//GEN-LAST:event_okBtnActionPerformed + + private void cancelBtnActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelBtnActionPerformed + dispose(); + }//GEN-LAST:event_cancelBtnActionPerformed + + private void identifierTextFieldActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_identifierTextFieldActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_identifierTextFieldActionPerformed + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton cancelBtn; + private javax.swing.JLabel identiferLbl; + private javax.swing.JTextField identifierTextField; + private javax.swing.JButton okBtn; + private javax.swing.JPanel settingsPanel; + private javax.swing.JComboBox typeComboBox; + private javax.swing.JLabel typeLbl; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/OpenPersonaManagerAction.java b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/OpenPersonasAction.java similarity index 88% rename from Core/src/org/sleuthkit/autopsy/centralrepository/persona/OpenPersonaManagerAction.java rename to Core/src/org/sleuthkit/autopsy/centralrepository/persona/OpenPersonasAction.java index 9d40eeb357..5407abc981 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/OpenPersonaManagerAction.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/OpenPersonasAction.java @@ -35,18 +35,18 @@ import org.sleuthkit.autopsy.coreutils.ThreadConfined; * An Action that opens the Persona Search window. */ -@ActionID(category = "Tools", id = "org.sleuthkit.autopsy.centralrepository.persona.OpenPersonaManagerAction") -@ActionRegistration(displayName = "#CTL_OpenPersonaManager", lazy = false) +@ActionID(category = "Tools", id = "org.sleuthkit.autopsy.centralrepository.persona.OpenPersonasAction") +@ActionRegistration(displayName = "#CTL_OpenPersonas", lazy = false) @ActionReferences(value = { @ActionReference(path = "Menu/Tools", position = 105) }) -public final class OpenPersonaManagerAction extends CallableSystemAction { +public final class OpenPersonasAction extends CallableSystemAction { private static final long serialVersionUID = 1L; private final JMenuItem menuItem; - public OpenPersonaManagerAction() { + public OpenPersonasAction() { menuItem = super.getMenuPresenter(); this.setEnabled(CentralRepository.isEnabled()); } @@ -54,7 +54,7 @@ public final class OpenPersonaManagerAction extends CallableSystemAction { @Override @ThreadConfined(type = ThreadConfined.ThreadType.AWT) public void performAction() { - final TopComponent topComponent = WindowManager.getDefault().findTopComponent("PersonaManagerTopComponent"); + final TopComponent topComponent = WindowManager.getDefault().findTopComponent("PersonasTopComponent"); if (topComponent != null) { if (topComponent.isOpened() == false) { topComponent.open(); @@ -65,7 +65,7 @@ public final class OpenPersonaManagerAction extends CallableSystemAction { } @Override - @NbBundle.Messages("OpenPersonasAction.displayName=Persona Manager") + @NbBundle.Messages("OpenPersonasAction.displayName=Personas") public String getName() { return Bundle.OpenPersonasAction_displayName(); } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddAccountDialog.form b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaAccountDialog.form similarity index 82% rename from Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddAccountDialog.form rename to Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaAccountDialog.form index c9cf66a7cb..05ad150c41 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddAccountDialog.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaAccountDialog.form @@ -25,24 +25,21 @@ - - - - - - - - - + + + + + + - + @@ -81,12 +78,12 @@ - + - + @@ -101,21 +98,21 @@ - + - - - - - - + + + + + + @@ -125,14 +122,14 @@ - + - + @@ -142,7 +139,7 @@ - + @@ -156,24 +153,10 @@ - - - - - - - - - - - - - - - + @@ -187,12 +170,26 @@ + + + + + + + + + + + + + + - + @@ -211,7 +208,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddAccountDialog.java b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaAccountDialog.java similarity index 68% rename from Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddAccountDialog.java rename to Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaAccountDialog.java index ffee3a3c22..558f92619e 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddAccountDialog.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaAccountDialog.java @@ -23,11 +23,12 @@ import java.io.Serializable; import java.util.Collection; import java.util.logging.Level; import javax.swing.JDialog; -import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.ListCellRenderer; +import javax.swing.SwingUtilities; +import org.apache.commons.lang.StringUtils; import org.openide.util.NbBundle.Messages; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoAccount; @@ -36,37 +37,61 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; import org.sleuthkit.autopsy.centralrepository.datamodel.Persona; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.InvalidAccountIDException; /** * Configuration dialog for adding an account to a persona. */ @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives -public class AddAccountDialog extends JDialog { +public class PersonaAccountDialog extends JDialog { - private static final Logger logger = Logger.getLogger(AddAccountDialog.class.getName()); + private static final Logger logger = Logger.getLogger(PersonaAccountDialog.class.getName()); private static final long serialVersionUID = 1L; - + private final TypeChoiceRenderer TYPE_CHOICE_RENDERER = new TypeChoiceRenderer(); private final PersonaDetailsPanel pdp; - + + private PersonaDetailsPanel.PAccount currentAccount = null; + /** * Creates new add account dialog */ - @Messages({"AddAccountDialog.title.text=Add Account",}) - public AddAccountDialog(PersonaDetailsPanel pdp) { - super((JFrame) WindowManager.getDefault().getMainWindow(), - Bundle.AddAccountDialog_title_text(), - true); + @Messages({"PersonaAccountDialog.title.text=Add Account",}) + public PersonaAccountDialog(PersonaDetailsPanel pdp) { + super(SwingUtilities.windowForComponent(pdp), + Bundle.PersonaAccountDialog_title_text(), + ModalityType.APPLICATION_MODAL); this.pdp = pdp; initComponents(); typeComboBox.setRenderer(TYPE_CHOICE_RENDERER); display(); } - + + PersonaAccountDialog(PersonaDetailsPanel pdp, PersonaDetailsPanel.PAccount acc) { + super(SwingUtilities.windowForComponent(pdp), + Bundle.PersonaAccountDialog_title_text(), + ModalityType.APPLICATION_MODAL); + this.pdp = pdp; + + initComponents(); + currentAccount = acc; + confidenceComboBox.setSelectedItem(acc.confidence); + justificationTextField.setText(acc.justification); + typeComboBox.setSelectedItem(acc.account.getAccountType()); + identifierTextField.setText(acc.account.getIdentifier()); + + typeComboBox.setEnabled(false); + identifierTextField.setEnabled(false); + + typeComboBox.setRenderer(TYPE_CHOICE_RENDERER); + display(); + } + /** - * This class handles displaying and rendering drop down menu for account choices + * This class handles displaying and rendering drop down menu for account + * choices */ private class TypeChoiceRenderer extends JLabel implements ListCellRenderer, Serializable { @@ -80,11 +105,10 @@ public class AddAccountDialog extends JDialog { return this; } } - + @Messages({ - "AddAccountDialog_get_types_exception_Title=Central Repository failure", - "AddAccountDialog_get_types_exception_msg=Failed to access central repository", - }) + "PersonaAccountDialog_get_types_exception_Title=Central Repository failure", + "PersonaAccountDialog_get_types_exception_msg=Failed to access central repository.",}) private CentralRepoAccountType[] getAllAccountTypes() { Collection allAccountTypes; try { @@ -92,9 +116,9 @@ public class AddAccountDialog extends JDialog { } catch (CentralRepoException e) { logger.log(Level.SEVERE, "Failed to access central repository", e); JOptionPane.showMessageDialog(this, - Bundle.AddAccountDialog_get_types_exception_Title(), - Bundle.AddAccountDialog_get_types_exception_msg(), - JOptionPane.ERROR_MESSAGE); + Bundle.PersonaAccountDialog_get_types_exception_Title(), + Bundle.PersonaAccountDialog_get_types_exception_msg(), + JOptionPane.ERROR_MESSAGE); return new CentralRepoAccountType[0]; } return allAccountTypes.toArray(new CentralRepoAccountType[0]); @@ -114,10 +138,10 @@ public class AddAccountDialog extends JDialog { identifierTextField = new javax.swing.JTextField(); typeLbl = new javax.swing.JLabel(); typeComboBox = new javax.swing.JComboBox<>(); - justificationLbl = new javax.swing.JLabel(); - justificationTextField = new javax.swing.JTextField(); confidenceLbl = new javax.swing.JLabel(); confidenceComboBox = new javax.swing.JComboBox<>(); + justificationLbl = new javax.swing.JLabel(); + justificationTextField = new javax.swing.JTextField(); cancelBtn = new javax.swing.JButton(); okBtn = new javax.swing.JButton(); @@ -126,27 +150,27 @@ public class AddAccountDialog extends JDialog { settingsPanel.setBorder(javax.swing.BorderFactory.createEtchedBorder()); - org.openide.awt.Mnemonics.setLocalizedText(identiferLbl, org.openide.util.NbBundle.getMessage(AddAccountDialog.class, "AddAccountDialog.identiferLbl.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(identiferLbl, org.openide.util.NbBundle.getMessage(PersonaAccountDialog.class, "PersonaAccountDialog.identiferLbl.text")); // NOI18N - identifierTextField.setText(org.openide.util.NbBundle.getMessage(AddAccountDialog.class, "AddAccountDialog.identifierTextField.text")); // NOI18N + identifierTextField.setText(org.openide.util.NbBundle.getMessage(PersonaAccountDialog.class, "PersonaAccountDialog.identifierTextField.text")); // NOI18N identifierTextField.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { identifierTextFieldActionPerformed(evt); } }); - org.openide.awt.Mnemonics.setLocalizedText(typeLbl, org.openide.util.NbBundle.getMessage(AddAccountDialog.class, "AddAccountDialog.typeLbl.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(typeLbl, org.openide.util.NbBundle.getMessage(PersonaAccountDialog.class, "PersonaAccountDialog.typeLbl.text")); // NOI18N typeComboBox.setModel(new javax.swing.DefaultComboBoxModel<>(getAllAccountTypes())); - org.openide.awt.Mnemonics.setLocalizedText(justificationLbl, org.openide.util.NbBundle.getMessage(AddAccountDialog.class, "AddAccountDialog.justificationLbl.text")); // NOI18N - - justificationTextField.setText(org.openide.util.NbBundle.getMessage(AddAccountDialog.class, "AddAccountDialog.justificationTextField.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(confidenceLbl, org.openide.util.NbBundle.getMessage(AddAccountDialog.class, "AddAccountDialog.confidenceLbl.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(confidenceLbl, org.openide.util.NbBundle.getMessage(PersonaAccountDialog.class, "PersonaAccountDialog.confidenceLbl.text")); // NOI18N confidenceComboBox.setModel(new javax.swing.DefaultComboBoxModel<>(org.sleuthkit.autopsy.centralrepository.datamodel.Persona.Confidence.values())); + org.openide.awt.Mnemonics.setLocalizedText(justificationLbl, org.openide.util.NbBundle.getMessage(PersonaAccountDialog.class, "PersonaAccountDialog.justificationLbl.text")); // NOI18N + + justificationTextField.setText(org.openide.util.NbBundle.getMessage(PersonaAccountDialog.class, "PersonaAccountDialog.justificationTextField.text")); // NOI18N + javax.swing.GroupLayout settingsPanelLayout = new javax.swing.GroupLayout(settingsPanel); settingsPanel.setLayout(settingsPanelLayout); settingsPanelLayout.setHorizontalGroup( @@ -165,11 +189,11 @@ public class AddAccountDialog extends JDialog { .addGroup(settingsPanelLayout.createSequentialGroup() .addComponent(confidenceLbl) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(confidenceComboBox, 0, 269, Short.MAX_VALUE)) + .addComponent(confidenceComboBox, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) .addGroup(settingsPanelLayout.createSequentialGroup() .addComponent(justificationLbl) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(justificationTextField, javax.swing.GroupLayout.DEFAULT_SIZE, 264, Short.MAX_VALUE))) + .addComponent(justificationTextField))) .addContainerGap()) ); settingsPanelLayout.setVerticalGroup( @@ -179,22 +203,22 @@ public class AddAccountDialog extends JDialog { .addGroup(settingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(identiferLbl) .addComponent(identifierTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(settingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(typeComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(typeLbl, javax.swing.GroupLayout.PREFERRED_SIZE, 9, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addGroup(settingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(justificationLbl) - .addComponent(justificationTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(settingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(confidenceLbl) .addComponent(confidenceComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(settingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(justificationLbl) + .addComponent(justificationTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); - org.openide.awt.Mnemonics.setLocalizedText(cancelBtn, org.openide.util.NbBundle.getMessage(AddAccountDialog.class, "AddAccountDialog.cancelBtn.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(cancelBtn, org.openide.util.NbBundle.getMessage(PersonaAccountDialog.class, "PersonaAccountDialog.cancelBtn.text")); // NOI18N cancelBtn.setMaximumSize(new java.awt.Dimension(79, 23)); cancelBtn.setMinimumSize(new java.awt.Dimension(79, 23)); cancelBtn.setPreferredSize(new java.awt.Dimension(79, 23)); @@ -204,7 +228,7 @@ public class AddAccountDialog extends JDialog { } }); - org.openide.awt.Mnemonics.setLocalizedText(okBtn, org.openide.util.NbBundle.getMessage(AddAccountDialog.class, "AddAccountDialog.okBtn.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(okBtn, org.openide.util.NbBundle.getMessage(PersonaAccountDialog.class, "PersonaAccountDialog.okBtn.text")); // NOI18N okBtn.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { okBtnActionPerformed(evt); @@ -216,14 +240,12 @@ public class AddAccountDialog extends JDialog { layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(settingsPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(layout.createSequentialGroup() - .addContainerGap(202, Short.MAX_VALUE) - .addComponent(okBtn) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(cancelBtn, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addContainerGap(194, Short.MAX_VALUE) + .addComponent(okBtn) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cancelBtn, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addContainerGap()) + .addComponent(settingsPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) ); layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {cancelBtn, okBtn}); @@ -231,8 +253,9 @@ public class AddAccountDialog extends JDialog { layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() + .addContainerGap() .addComponent(settingsPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(okBtn, javax.swing.GroupLayout.Alignment.TRAILING) .addComponent(cancelBtn, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) @@ -248,19 +271,29 @@ public class AddAccountDialog extends JDialog { } @Messages({ - "AddAccountDialog_dup_Title=Account add failure", - "AddAccountDialog_dup_msg=This account is already added to the persona", - "AddAccountDialog_empty_Title=Empty identifier", - "AddAccountDialog_empty_msg=The identifier field cannot be empty", - "AddAccountDialog_search_failure_Title=Account add failure", - "AddAccountDialog_search_failure_msg=Central Repository account search failed", - "AddAccountDialog_search_empty_Title=Account not found", - "AddAccountDialog_search_empty_msg=Account not found for given identifier and type",}) + "PersonaAccountDialog_dup_Title=Account add failure", + "PersonaAccountDialog_dup_msg=This account is already added to the persona.", + "PersonaAccountDialog_identifier_empty_Title=Empty identifier", + "PersonaAccountDialog_identifier_empty_msg=The identifier field cannot be empty.", + "PersonaAccountDialog_search_failure_Title=Account add failure", + "PersonaAccountDialog_search_failure_msg=Central Repository account search failed.", + "PersonaAccountDialog_search_empty_Title=Account not found", + "PersonaAccountDialog_search_empty_msg=Account not found for given identifier and type.", + "PersonaAccountDialog_invalid_account_Title=Invalid account identifier", + "PersonaAccountDialog_invalid_account_msg=Account identifier is not valid.", + }) private void okBtnActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okBtnActionPerformed - if (identifierTextField.getText().isEmpty()) { + if (StringUtils.isBlank(identifierTextField.getText())) { JOptionPane.showMessageDialog(this, - Bundle.AddAccountDialog_empty_msg(), - Bundle.AddAccountDialog_empty_Title(), + Bundle.PersonaAccountDialog_identifier_empty_msg(), + Bundle.PersonaAccountDialog_identifier_empty_Title(), + JOptionPane.ERROR_MESSAGE); + return; + } + if (StringUtils.isBlank(justificationTextField.getText())) { + JOptionPane.showMessageDialog(this, + Bundle.PersonaDetailsPanel_empty_justification_msg(), + Bundle.PersonaDetailsPanel_empty_justification_Title(), JOptionPane.ERROR_MESSAGE); return; } @@ -270,15 +303,23 @@ public class AddAccountDialog extends JDialog { } catch (CentralRepoException e) { logger.log(Level.SEVERE, "Failed to access central repository", e); JOptionPane.showMessageDialog(this, - Bundle.AddAccountDialog_search_failure_msg(), - Bundle.AddAccountDialog_search_failure_Title(), + Bundle.PersonaAccountDialog_search_failure_msg(), + Bundle.PersonaAccountDialog_search_failure_Title(), + JOptionPane.ERROR_MESSAGE); + return; + } + catch (InvalidAccountIDException e) { + logger.log(Level.SEVERE, "Invalid account identifier", e); + JOptionPane.showMessageDialog(this, + Bundle.PersonaAccountDialog_invalid_account_msg(), + Bundle.PersonaAccountDialog_invalid_account_Title(), JOptionPane.ERROR_MESSAGE); return; } if (candidates.isEmpty()) { JOptionPane.showMessageDialog(this, - Bundle.AddAccountDialog_search_empty_msg(), - Bundle.AddAccountDialog_search_empty_Title(), + Bundle.PersonaAccountDialog_search_empty_msg(), + Bundle.PersonaAccountDialog_search_empty_Title(), JOptionPane.ERROR_MESSAGE); return; } @@ -292,22 +333,28 @@ public class AddAccountDialog extends JDialog { } if (result == null) { JOptionPane.showMessageDialog(this, - Bundle.AddAccountDialog_search_empty_msg(), - Bundle.AddAccountDialog_search_empty_Title(), + Bundle.PersonaAccountDialog_search_empty_msg(), + Bundle.PersonaAccountDialog_search_empty_Title(), JOptionPane.ERROR_MESSAGE); return; } - - if (pdp.addAccount( - result, - justificationTextField.getText(), - (Persona.Confidence) confidenceComboBox.getSelectedItem())) { + + Persona.Confidence confidence = (Persona.Confidence) confidenceComboBox.getSelectedItem(); + String justification = justificationTextField.getText(); + + if (currentAccount != null) { + currentAccount.confidence = confidence; + currentAccount.justification = justification; dispose(); } else { - JOptionPane.showMessageDialog(this, - Bundle.AddAccountDialog_dup_msg(), - Bundle.AddAccountDialog_dup_Title(), - JOptionPane.ERROR_MESSAGE); + if (pdp.addAccount(result, justification, confidence)) { + dispose(); + } else { + JOptionPane.showMessageDialog(this, + Bundle.PersonaAccountDialog_dup_msg(), + Bundle.PersonaAccountDialog_dup_Title(), + JOptionPane.ERROR_MESSAGE); + } } }//GEN-LAST:event_okBtnActionPerformed @@ -318,7 +365,7 @@ public class AddAccountDialog extends JDialog { private void identifierTextFieldActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_identifierTextFieldActionPerformed // TODO add your handling code here: }//GEN-LAST:event_identifierTextFieldActionPerformed - + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton cancelBtn; private javax.swing.JComboBox confidenceComboBox; diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddAliasDialog.form b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaAliasDialog.form similarity index 88% rename from Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddAliasDialog.form rename to Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaAliasDialog.form index 510a5a116f..37b76a6375 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddAliasDialog.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaAliasDialog.form @@ -38,7 +38,7 @@ - + @@ -92,17 +92,17 @@ - - - - - - + + + + + + @@ -111,35 +111,21 @@ - + - - - - - - - - - - - - - - - + - + @@ -153,12 +139,26 @@ + + + + + + + + + + + + + + - + @@ -177,7 +177,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddAliasDialog.java b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaAliasDialog.java similarity index 74% rename from Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddAliasDialog.java rename to Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaAliasDialog.java index 632b5b785c..1ada0a1025 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddAliasDialog.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaAliasDialog.java @@ -19,8 +19,9 @@ package org.sleuthkit.autopsy.centralrepository.persona; import javax.swing.JDialog; -import javax.swing.JFrame; import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; +import org.apache.commons.lang.StringUtils; import org.openide.util.NbBundle.Messages; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.centralrepository.datamodel.Persona; @@ -29,26 +30,45 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.Persona; * Configuration dialog for adding aliases to a persona. */ @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives -public class AddAliasDialog extends JDialog { +public class PersonaAliasDialog extends JDialog { private static final long serialVersionUID = 1L; private final PersonaDetailsPanel pdp; + private PersonaDetailsPanel.PAlias currentAlias = null; + /** * Creates new add alias dialog */ - @Messages({"AddAliasDialog.title.text=Add Alias",}) - public AddAliasDialog(PersonaDetailsPanel pdp) { - super((JFrame) WindowManager.getDefault().getMainWindow(), - Bundle.AddAliasDialog_title_text(), - true); + @Messages({"PersonaAliasDialog.title.text=Add Alias",}) + public PersonaAliasDialog(PersonaDetailsPanel pdp) { + super(SwingUtilities.windowForComponent(pdp), + Bundle.PersonaAliasDialog_title_text(), + ModalityType.APPLICATION_MODAL); this.pdp = pdp; initComponents(); display(); } + PersonaAliasDialog(PersonaDetailsPanel pdp, PersonaDetailsPanel.PAlias pa) { + super(SwingUtilities.windowForComponent(pdp), + Bundle.PersonaAliasDialog_title_text(), + ModalityType.APPLICATION_MODAL); + this.pdp = pdp; + + initComponents(); + currentAlias = pa; + confidenceComboBox.setSelectedItem(pa.confidence); + justificationTextField.setText(pa.justification); + aliasTextField.setText(pa.alias); + + aliasTextField.setEnabled(false); + + display(); + } + /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always @@ -61,10 +81,10 @@ public class AddAliasDialog extends JDialog { settingsPanel = new javax.swing.JPanel(); aliasLbl = new javax.swing.JLabel(); aliasTextField = new javax.swing.JTextField(); - justificationLbl = new javax.swing.JLabel(); - justificationTextField = new javax.swing.JTextField(); confidenceLbl = new javax.swing.JLabel(); confidenceComboBox = new javax.swing.JComboBox<>(); + justificationLbl = new javax.swing.JLabel(); + justificationTextField = new javax.swing.JTextField(); cancelBtn = new javax.swing.JButton(); okBtn = new javax.swing.JButton(); @@ -73,18 +93,18 @@ public class AddAliasDialog extends JDialog { settingsPanel.setBorder(javax.swing.BorderFactory.createEtchedBorder()); - org.openide.awt.Mnemonics.setLocalizedText(aliasLbl, org.openide.util.NbBundle.getMessage(AddAliasDialog.class, "AddAliasDialog.aliasLbl.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(aliasLbl, org.openide.util.NbBundle.getMessage(PersonaAliasDialog.class, "PersonaAliasDialog.aliasLbl.text")); // NOI18N - aliasTextField.setText(org.openide.util.NbBundle.getMessage(AddAliasDialog.class, "AddAliasDialog.aliasTextField.text")); // NOI18N + aliasTextField.setText(org.openide.util.NbBundle.getMessage(PersonaAliasDialog.class, "PersonaAliasDialog.aliasTextField.text")); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(justificationLbl, org.openide.util.NbBundle.getMessage(AddAliasDialog.class, "AddAliasDialog.justificationLbl.text")); // NOI18N - - justificationTextField.setText(org.openide.util.NbBundle.getMessage(AddAliasDialog.class, "AddAliasDialog.justificationTextField.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(confidenceLbl, org.openide.util.NbBundle.getMessage(AddAliasDialog.class, "AddAliasDialog.confidenceLbl.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(confidenceLbl, org.openide.util.NbBundle.getMessage(PersonaAliasDialog.class, "PersonaAliasDialog.confidenceLbl.text")); // NOI18N confidenceComboBox.setModel(new javax.swing.DefaultComboBoxModel<>(org.sleuthkit.autopsy.centralrepository.datamodel.Persona.Confidence.values())); + org.openide.awt.Mnemonics.setLocalizedText(justificationLbl, org.openide.util.NbBundle.getMessage(PersonaAliasDialog.class, "PersonaAliasDialog.justificationLbl.text")); // NOI18N + + justificationTextField.setText(org.openide.util.NbBundle.getMessage(PersonaAliasDialog.class, "PersonaAliasDialog.justificationTextField.text")); // NOI18N + javax.swing.GroupLayout settingsPanelLayout = new javax.swing.GroupLayout(settingsPanel); settingsPanel.setLayout(settingsPanelLayout); settingsPanelLayout.setHorizontalGroup( @@ -114,17 +134,17 @@ public class AddAliasDialog extends JDialog { .addComponent(aliasLbl) .addComponent(aliasTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(settingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(justificationTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(justificationLbl)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addGroup(settingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(confidenceComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(confidenceLbl)) - .addContainerGap()) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(settingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(justificationTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(justificationLbl)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); - org.openide.awt.Mnemonics.setLocalizedText(cancelBtn, org.openide.util.NbBundle.getMessage(AddAliasDialog.class, "AddAliasDialog.cancelBtn.text_1")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(cancelBtn, org.openide.util.NbBundle.getMessage(PersonaAliasDialog.class, "PersonaAliasDialog.cancelBtn.text_1")); // NOI18N cancelBtn.setMaximumSize(new java.awt.Dimension(79, 23)); cancelBtn.setMinimumSize(new java.awt.Dimension(79, 23)); cancelBtn.setPreferredSize(new java.awt.Dimension(79, 23)); @@ -134,7 +154,7 @@ public class AddAliasDialog extends JDialog { } }); - org.openide.awt.Mnemonics.setLocalizedText(okBtn, org.openide.util.NbBundle.getMessage(AddAliasDialog.class, "AddAliasDialog.okBtn.text_1")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(okBtn, org.openide.util.NbBundle.getMessage(PersonaAliasDialog.class, "PersonaAliasDialog.okBtn.text_1")); // NOI18N okBtn.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { okBtnActionPerformed(evt); @@ -176,19 +196,42 @@ public class AddAliasDialog extends JDialog { } @Messages({ - "AddAliasDialog_dup_Title=Alias add failure", - "AddAliasDialog_dup_msg=This alias has already been added to this persona",}) + "PersonaAliasDialog_empty_Title=Empty alias", + "PersonaAliasDialog_empty_msg=An alias cannot be empty.", + "PersonaAliasDialog_dup_Title=Alias add failure", + "PersonaAliasDialog_dup_msg=This alias has already been added to this persona.",}) private void okBtnActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okBtnActionPerformed - if (pdp.addAlias( - aliasTextField.getText(), - justificationTextField.getText(), - (Persona.Confidence) confidenceComboBox.getSelectedItem())) { + if (StringUtils.isBlank(aliasTextField.getText())) { + JOptionPane.showMessageDialog(this, + Bundle.PersonaAliasDialog_empty_msg(), + Bundle.PersonaAliasDialog_empty_Title(), + JOptionPane.ERROR_MESSAGE); + return; + } + if (StringUtils.isBlank(justificationTextField.getText())) { + JOptionPane.showMessageDialog(this, + Bundle.PersonaDetailsPanel_empty_justification_msg(), + Bundle.PersonaDetailsPanel_empty_justification_Title(), + JOptionPane.ERROR_MESSAGE); + return; + } + + Persona.Confidence confidence = (Persona.Confidence) confidenceComboBox.getSelectedItem(); + String justification = justificationTextField.getText(); + + if (currentAlias != null) { + currentAlias.confidence = confidence; + currentAlias.justification = justification; dispose(); } else { - JOptionPane.showMessageDialog(this, - Bundle.AddAliasDialog_dup_msg(), - Bundle.AddAliasDialog_dup_Title(), + if (pdp.addAlias(aliasTextField.getText(), justification, confidence)) { + dispose(); + } else { + JOptionPane.showMessageDialog(this, + Bundle.PersonaAliasDialog_dup_msg(), + Bundle.PersonaAliasDialog_dup_Title(), JOptionPane.ERROR_MESSAGE); + } } }//GEN-LAST:event_okBtnActionPerformed diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaDetailsDialog.java b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaDetailsDialog.java index 8889f023b6..187e553a8f 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaDetailsDialog.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaDetailsDialog.java @@ -44,6 +44,10 @@ public class PersonaDetailsDialog extends JDialog { "PersonaDetailsDialogEditTitle=Edit Persona", "PersonaDetailsDialogViewTitle=View Persona",}) public PersonaDetailsDialog(Component parent, PersonaDetailsMode mode, Persona persona, PersonaDetailsDialogCallback callback) { + // by default, display the dialog right away + this(parent, mode, persona, callback, true); + } + public PersonaDetailsDialog(Component parent, PersonaDetailsMode mode, Persona persona, PersonaDetailsDialogCallback callback, boolean displayDialog) { super((JFrame) WindowManager.getDefault().getMainWindow(), getTitle(mode), true); @@ -53,7 +57,9 @@ public class PersonaDetailsDialog extends JDialog { pdp.setMode(parent, mode, persona); - display(); + if (displayDialog) { + display(); + } } private static String getTitle(PersonaDetailsMode mode) { @@ -136,7 +142,7 @@ public class PersonaDetailsDialog extends JDialog { pack(); }// //GEN-END:initComponents - private void display() { + public final void display() { this.setLocationRelativeTo(WindowManager.getDefault().getMainWindow()); setVisible(true); } @@ -153,6 +159,10 @@ public class PersonaDetailsDialog extends JDialog { dispose(); }//GEN-LAST:event_cancelBtnActionPerformed + public PersonaDetailsPanel getDetailsPanel() { + return this.pdp; + } + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton cancelBtn; private javax.swing.JButton okBtn; diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaDetailsPanel.form b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaDetailsPanel.form index c4200d166b..ded4f0f448 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaDetailsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaDetailsPanel.form @@ -37,17 +37,20 @@ - + - + - + - + + + + @@ -61,12 +64,7 @@ - - - - - - + @@ -74,31 +72,51 @@ + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + @@ -107,7 +125,19 @@ + + + + + + + + + + + + @@ -120,40 +150,83 @@ + - - - + + + + - - - + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -168,9 +241,6 @@ - - - @@ -203,6 +273,14 @@ + + + + + + + + @@ -242,6 +320,14 @@ + + + + + + + + @@ -281,6 +367,14 @@ + + + + + + + + @@ -312,22 +406,6 @@ - - - - - - - - - - - - - - - - diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaDetailsPanel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaDetailsPanel.java index a7c76b7ad4..edcb36c353 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaDetailsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaDetailsPanel.java @@ -20,9 +20,13 @@ package org.sleuthkit.autopsy.centralrepository.persona; import java.awt.Component; import java.awt.event.ActionEvent; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.Date; import java.util.List; +import java.util.Map; import java.util.logging.Level; import javax.swing.JButton; import javax.swing.JOptionPane; @@ -30,11 +34,14 @@ import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.event.ListSelectionEvent; import javax.swing.table.DefaultTableModel; +import org.apache.commons.lang.StringUtils; import org.openide.windows.TopComponent; import org.openide.util.NbBundle.Messages; import org.openide.windows.RetainLocation; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoAccount; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoExaminer; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationCase; import org.sleuthkit.autopsy.centralrepository.datamodel.Persona; import org.sleuthkit.autopsy.centralrepository.datamodel.PersonaAccount; @@ -65,6 +72,10 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { private final List metadataToRemove = new ArrayList<>(); private final List aliasesToRemove = new ArrayList<>(); + private final Map accountsToEdit = new HashMap<>(); + private final Map metadataToEdit = new HashMap<>(); + private final Map aliasesToEdit = new HashMap<>(); + private Persona currentPersona; private List currentAccounts = new ArrayList<>(); private List currentMetadata = new ArrayList<>(); @@ -76,13 +87,29 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { private PersonaDetailsTableModel aliasesModel; private PersonaDetailsTableModel casesModel; + @Messages({ + "PersonaDetailsPanel_empty_justification_Title=Empty justification", + "PersonaDetailsPanel_empty_justification_msg=The justification field cannot be empty",}) public PersonaDetailsPanel() { initComponents(); clear(); // Accounts addAccountBtn.addActionListener((ActionEvent e) -> { - new AddAccountDialog(this); + new PersonaAccountDialog(this); + }); + editAccountBtn.addActionListener((ActionEvent e) -> { + int selectedRow = accountsTable.getSelectedRow(); + if (selectedRow != -1) { + if (selectedRow >= currentAccounts.size()) { + PAccount acc = accountsToAdd.get(selectedRow - currentAccounts.size()); + new PersonaAccountDialog(this, acc); + } else { + PersonaAccount personaAccount = currentAccounts.get(selectedRow); + accountsToEdit.putIfAbsent(personaAccount, new PAccount(personaAccount.getAccount(), personaAccount.getJustification(), personaAccount.getConfidence())); + new PersonaAccountDialog(this, accountsToEdit.get(personaAccount)); + } + } }); deleteAccountBtn.addActionListener((ActionEvent e) -> { int selectedRow = accountsTable.getSelectedRow(); @@ -91,20 +118,35 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { if (selectedRow >= currentAccounts.size()) { accountsToAdd.remove(selectedRow - currentAccounts.size()); } else { - accountsToRemove.add(currentAccounts.get(selectedRow)); - currentAccounts.remove(selectedRow); + PersonaAccount toRemove = currentAccounts.get(selectedRow); + accountsToEdit.remove(toRemove); + accountsToRemove.add(toRemove); + currentAccounts.remove(toRemove); } updateAccountsTable(); } }); accountsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); accountsTable.getSelectionModel().addListSelectionListener((ListSelectionEvent e) -> { - handleSelectionChange(e, deleteAccountBtn, accountsTable); + handleSelectionChange(e, editAccountBtn, deleteAccountBtn, accountsTable); }); // Metadata addMetadataBtn.addActionListener((ActionEvent e) -> { - new AddMetadataDialog(this); + new PersonaMetadataDialog(this); + }); + editMetadataBtn.addActionListener((ActionEvent e) -> { + int selectedRow = metadataTable.getSelectedRow(); + if (selectedRow != -1) { + if (selectedRow >= currentMetadata.size()) { + PMetadata md = metadataToAdd.get(selectedRow - currentMetadata.size()); + new PersonaMetadataDialog(this, md); + } else { + PersonaMetadata personaMetadata = currentMetadata.get(selectedRow); + metadataToEdit.putIfAbsent(personaMetadata, new PMetadata(personaMetadata.getName(), personaMetadata.getValue(), personaMetadata.getJustification(), personaMetadata.getConfidence())); + new PersonaMetadataDialog(this, metadataToEdit.get(personaMetadata)); + } + } }); deleteMetadataBtn.addActionListener((ActionEvent e) -> { int selectedRow = metadataTable.getSelectedRow(); @@ -113,20 +155,35 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { if (selectedRow >= currentMetadata.size()) { metadataToAdd.remove(selectedRow - currentMetadata.size()); } else { - metadataToRemove.add(currentMetadata.get(selectedRow)); - currentMetadata.remove(selectedRow); + PersonaMetadata toRemove = currentMetadata.get(selectedRow); + metadataToEdit.remove(toRemove); + metadataToRemove.add(toRemove); + currentMetadata.remove(toRemove); } updateMetadataTable(); } }); metadataTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); metadataTable.getSelectionModel().addListSelectionListener((ListSelectionEvent e) -> { - handleSelectionChange(e, deleteMetadataBtn, metadataTable); + handleSelectionChange(e, editMetadataBtn, deleteMetadataBtn, metadataTable); }); // Aliases addAliasBtn.addActionListener((ActionEvent e) -> { - new AddAliasDialog(this); + new PersonaAliasDialog(this); + }); + editAliasBtn.addActionListener((ActionEvent e) -> { + int selectedRow = aliasesTable.getSelectedRow(); + if (selectedRow != -1) { + if (selectedRow >= currentAliases.size()) { + PAlias pa = aliasesToAdd.get(selectedRow - currentAliases.size()); + new PersonaAliasDialog(this, pa); + } else { + PersonaAlias personaAlias = currentAliases.get(selectedRow); + aliasesToEdit.putIfAbsent(personaAlias, new PAlias(personaAlias.getAlias(), personaAlias.getJustification(), personaAlias.getConfidence())); + new PersonaAliasDialog(this, aliasesToEdit.get(personaAlias)); + } + } }); deleteAliasBtn.addActionListener((ActionEvent e) -> { int selectedRow = aliasesTable.getSelectedRow(); @@ -135,33 +192,48 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { if (selectedRow >= currentAliases.size()) { aliasesToAdd.remove(selectedRow - currentAliases.size()); } else { - aliasesToRemove.add(currentAliases.get(selectedRow)); - currentAliases.remove(selectedRow); + PersonaAlias toRemove = currentAliases.get(selectedRow); + aliasesToEdit.remove(toRemove); + aliasesToRemove.add(toRemove); + currentAliases.remove(toRemove); } updateAliasesTable(); } }); aliasesTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); aliasesTable.getSelectionModel().addListSelectionListener((ListSelectionEvent e) -> { - handleSelectionChange(e, deleteAliasBtn, aliasesTable); + handleSelectionChange(e, editAliasBtn, deleteAliasBtn, aliasesTable); }); } - private void handleSelectionChange(ListSelectionEvent e, JButton btn, JTable table) { + private void handleSelectionChange(ListSelectionEvent e, JButton editBtn, JButton deleteBtn, JTable table) { if (e.getValueIsAdjusting()) { return; } - btn.setEnabled(mode != PersonaDetailsMode.VIEW && table.getSelectedRow() != -1); + editBtn.setEnabled(mode != PersonaDetailsMode.VIEW && table.getSelectedRow() != -1); + deleteBtn.setEnabled(mode != PersonaDetailsMode.VIEW && table.getSelectedRow() != -1); + } + + void addEditExistingAccount(PersonaAccount account, String justification, Persona.Confidence confidence) { + accountsToEdit.put(account, new PAccount(account.getAccount(), justification, confidence)); + } + + void addEditExistingMetadata(PersonaMetadata metadata, String justification, Persona.Confidence confidence) { + metadataToEdit.put(metadata, new PMetadata(metadata.getName(), metadata.getValue(), justification, confidence)); + } + + void addEditExistingAlias(PersonaAlias alias, String justification, Persona.Confidence confidence) { + aliasesToEdit.put(alias, new PAlias(alias.getAlias(), justification, confidence)); } /** * A data bucket class for yet-to-be-created PersonaAccount */ - private class PAccount { + class PAccount { - private final CentralRepoAccount account; - private final String justification; - private final Persona.Confidence confidence; + CentralRepoAccount account; + String justification; + Persona.Confidence confidence; PAccount(CentralRepoAccount account, String justification, Persona.Confidence confidence) { this.account = account; @@ -182,9 +254,9 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { } } return false; - } + } - boolean addAccount(CentralRepoAccount account, String justification, Persona.Confidence confidence) { + public boolean addAccount(CentralRepoAccount account, String justification, Persona.Confidence confidence) { if (!accountExists(account)) { accountsToAdd.add(new PAccount(account, justification, confidence)); updateAccountsTable(); @@ -196,12 +268,12 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { /** * A data bucket class for yet-to-be-created PersonaMetadata */ - private class PMetadata { + class PMetadata { - private final String name; - private final String value; - private final String justification; - private final Persona.Confidence confidence; + String name; + String value; + String justification; + Persona.Confidence confidence; PMetadata(String name, String value, String justification, Persona.Confidence confidence) { this.name = name; @@ -237,11 +309,11 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { /** * A data bucket class for yet-to-be-created PersonaAlias */ - private class PAlias { + class PAlias { - private final String alias; - private final String justification; - private final Persona.Confidence confidence; + String alias; + String justification; + Persona.Confidence confidence; PAlias(String alias, String justification, Persona.Confidence confidence) { this.alias = alias; @@ -282,38 +354,55 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { private void initComponents() { detailsPanel = new javax.swing.JPanel(); + examinerLbl = new javax.swing.JLabel(); + examinerField = new javax.swing.JTextField(); + creationDateLbl = new javax.swing.JLabel(); + creationDateField = new javax.swing.JTextField(); + commentLbl = new javax.swing.JLabel(); + commentField = new javax.swing.JTextField(); nameLbl = new javax.swing.JLabel(); nameField = new javax.swing.JTextField(); accountsLbl = new javax.swing.JLabel(); accountsTablePane = new javax.swing.JScrollPane(); accountsTable = new javax.swing.JTable(); addAccountBtn = new javax.swing.JButton(); + editAccountBtn = new javax.swing.JButton(); deleteAccountBtn = new javax.swing.JButton(); metadataLabel = new javax.swing.JLabel(); metadataTablePane = new javax.swing.JScrollPane(); metadataTable = new javax.swing.JTable(); addMetadataBtn = new javax.swing.JButton(); + editMetadataBtn = new javax.swing.JButton(); deleteMetadataBtn = new javax.swing.JButton(); aliasesLabel = new javax.swing.JLabel(); aliasesTablePane = new javax.swing.JScrollPane(); aliasesTable = new javax.swing.JTable(); addAliasBtn = new javax.swing.JButton(); + editAliasBtn = new javax.swing.JButton(); deleteAliasBtn = new javax.swing.JButton(); casesLbl = new javax.swing.JLabel(); casesTablePane = new javax.swing.JScrollPane(); casesTable = new javax.swing.JTable(); - addCaseBtn = new javax.swing.JButton(); - deleteCaseBtn = new javax.swing.JButton(); + + org.openide.awt.Mnemonics.setLocalizedText(examinerLbl, org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.examinerLbl.text")); // NOI18N + + examinerField.setEditable(false); + examinerField.setText(org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.examinerField.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(creationDateLbl, org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.creationDateLbl.text")); // NOI18N + + creationDateField.setEditable(false); + creationDateField.setText(org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.creationDateField.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(commentLbl, org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.commentLbl.text")); // NOI18N + + commentField.setEditable(false); + commentField.setText(org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.commentField.text")); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(nameLbl, org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.nameLbl.text")); // NOI18N nameField.setEditable(false); nameField.setText(org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.nameField.text")); // NOI18N - nameField.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - nameFieldActionPerformed(evt); - } - }); org.openide.awt.Mnemonics.setLocalizedText(accountsLbl, org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.accountsLbl.text")); // NOI18N @@ -333,6 +422,9 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { org.openide.awt.Mnemonics.setLocalizedText(addAccountBtn, org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.addAccountBtn.text")); // NOI18N addAccountBtn.setEnabled(false); + org.openide.awt.Mnemonics.setLocalizedText(editAccountBtn, org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.editAccountBtn.text")); // NOI18N + editAccountBtn.setEnabled(false); + org.openide.awt.Mnemonics.setLocalizedText(deleteAccountBtn, org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.deleteAccountBtn.text")); // NOI18N deleteAccountBtn.setEnabled(false); @@ -354,6 +446,9 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { org.openide.awt.Mnemonics.setLocalizedText(addMetadataBtn, org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.addMetadataBtn.text")); // NOI18N addMetadataBtn.setEnabled(false); + org.openide.awt.Mnemonics.setLocalizedText(editMetadataBtn, org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.editMetadataBtn.text")); // NOI18N + editMetadataBtn.setEnabled(false); + org.openide.awt.Mnemonics.setLocalizedText(deleteMetadataBtn, org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.deleteMetadataBtn.text")); // NOI18N deleteMetadataBtn.setEnabled(false); @@ -375,6 +470,9 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { org.openide.awt.Mnemonics.setLocalizedText(addAliasBtn, org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.addAliasBtn.text")); // NOI18N addAliasBtn.setEnabled(false); + org.openide.awt.Mnemonics.setLocalizedText(editAliasBtn, org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.editAliasBtn.text")); // NOI18N + editAliasBtn.setEnabled(false); + org.openide.awt.Mnemonics.setLocalizedText(deleteAliasBtn, org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.deleteAliasBtn.text")); // NOI18N deleteAliasBtn.setEnabled(false); @@ -393,12 +491,6 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { )); casesTablePane.setViewportView(casesTable); - org.openide.awt.Mnemonics.setLocalizedText(addCaseBtn, org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.addCaseBtn.text")); // NOI18N - addCaseBtn.setEnabled(false); - - org.openide.awt.Mnemonics.setLocalizedText(deleteCaseBtn, org.openide.util.NbBundle.getMessage(PersonaDetailsPanel.class, "PersonaDetailsPanel.deleteCaseBtn.text")); // NOI18N - deleteCaseBtn.setEnabled(false); - javax.swing.GroupLayout detailsPanelLayout = new javax.swing.GroupLayout(detailsPanel); detailsPanel.setLayout(detailsPanelLayout); detailsPanelLayout.setHorizontalGroup( @@ -406,11 +498,7 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { .addGroup(detailsPanelLayout.createSequentialGroup() .addContainerGap() .addGroup(detailsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(accountsTablePane, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 549, Short.MAX_VALUE) - .addGroup(detailsPanelLayout.createSequentialGroup() - .addComponent(nameLbl) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(nameField)) + .addComponent(accountsTablePane, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 605, Short.MAX_VALUE) .addComponent(accountsLbl, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(metadataLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(metadataTablePane, javax.swing.GroupLayout.Alignment.TRAILING) @@ -418,31 +506,59 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { .addComponent(aliasesTablePane) .addComponent(casesLbl, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(casesTablePane) + .addGroup(detailsPanelLayout.createSequentialGroup() + .addComponent(commentLbl) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(commentField)) + .addGroup(detailsPanelLayout.createSequentialGroup() + .addComponent(nameLbl) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(nameField)) .addGroup(detailsPanelLayout.createSequentialGroup() .addGroup(detailsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(detailsPanelLayout.createSequentialGroup() - .addComponent(addCaseBtn) - .addGap(18, 18, 18) - .addComponent(deleteCaseBtn)) .addGroup(detailsPanelLayout.createSequentialGroup() .addComponent(addAccountBtn) .addGap(18, 18, 18) + .addComponent(editAccountBtn) + .addGap(18, 18, 18) .addComponent(deleteAccountBtn)) .addGroup(detailsPanelLayout.createSequentialGroup() .addComponent(addMetadataBtn) .addGap(18, 18, 18) + .addComponent(editMetadataBtn) + .addGap(18, 18, 18) .addComponent(deleteMetadataBtn)) .addGroup(detailsPanelLayout.createSequentialGroup() .addComponent(addAliasBtn) .addGap(18, 18, 18) + .addComponent(editAliasBtn) + .addGap(18, 18, 18) .addComponent(deleteAliasBtn))) - .addGap(0, 0, Short.MAX_VALUE))) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(detailsPanelLayout.createSequentialGroup() + .addComponent(examinerLbl) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(examinerField, javax.swing.GroupLayout.PREFERRED_SIZE, 161, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, 18) + .addComponent(creationDateLbl) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(creationDateField))) .addContainerGap()) ); detailsPanelLayout.setVerticalGroup( detailsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(detailsPanelLayout.createSequentialGroup() - .addContainerGap() + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(detailsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(examinerLbl) + .addComponent(examinerField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(creationDateLbl) + .addComponent(creationDateField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(detailsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(commentField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(commentLbl)) + .addGap(20, 20, 20) .addGroup(detailsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(nameLbl) .addComponent(nameField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) @@ -453,61 +569,57 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(detailsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(addAccountBtn) - .addComponent(deleteAccountBtn)) + .addComponent(deleteAccountBtn) + .addComponent(editAccountBtn)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(metadataLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(metadataTablePane, javax.swing.GroupLayout.PREFERRED_SIZE, 66, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(detailsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(detailsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(addMetadataBtn) .addComponent(deleteMetadataBtn) - .addComponent(addMetadataBtn)) + .addComponent(editMetadataBtn)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(aliasesLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(aliasesTablePane, javax.swing.GroupLayout.PREFERRED_SIZE, 74, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(detailsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(detailsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(addAliasBtn) .addComponent(deleteAliasBtn) - .addComponent(addAliasBtn)) + .addComponent(editAliasBtn)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(casesLbl) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(casesTablePane, javax.swing.GroupLayout.PREFERRED_SIZE, 63, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(detailsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(addCaseBtn) - .addComponent(deleteCaseBtn)) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addContainerGap()) ); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 561, Short.MAX_VALUE) + .addComponent(detailsPanel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(detailsPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGap(0, 617, Short.MAX_VALUE)) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 559, Short.MAX_VALUE) + .addGap(0, 521, Short.MAX_VALUE) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(detailsPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addComponent(detailsPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(0, 0, Short.MAX_VALUE))) ); }// //GEN-END:initComponents - private void nameFieldActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_nameFieldActionPerformed - - }//GEN-LAST:event_nameFieldActionPerformed - // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JLabel accountsLbl; private javax.swing.JTable accountsTable; private javax.swing.JScrollPane accountsTablePane; private javax.swing.JButton addAccountBtn; private javax.swing.JButton addAliasBtn; - private javax.swing.JButton addCaseBtn; private javax.swing.JButton addMetadataBtn; private javax.swing.JLabel aliasesLabel; private javax.swing.JTable aliasesTable; @@ -515,11 +627,19 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { private javax.swing.JLabel casesLbl; private javax.swing.JTable casesTable; private javax.swing.JScrollPane casesTablePane; + private javax.swing.JTextField commentField; + private javax.swing.JLabel commentLbl; + private javax.swing.JTextField creationDateField; + private javax.swing.JLabel creationDateLbl; private javax.swing.JButton deleteAccountBtn; private javax.swing.JButton deleteAliasBtn; - private javax.swing.JButton deleteCaseBtn; private javax.swing.JButton deleteMetadataBtn; private javax.swing.JPanel detailsPanel; + private javax.swing.JButton editAccountBtn; + private javax.swing.JButton editAliasBtn; + private javax.swing.JButton editMetadataBtn; + private javax.swing.JTextField examinerField; + private javax.swing.JLabel examinerLbl; private javax.swing.JLabel metadataLabel; private javax.swing.JTable metadataTable; private javax.swing.JScrollPane metadataTablePane; @@ -529,14 +649,24 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { @Messages({ "PersonaDetailsPanel_load_exception_Title=Initialization failure", - "PersonaDetailsPanel_load_exception_msg=Failed to load persona",}) + "PersonaDetailsPanel_load_exception_msg=Failed to load persona.",}) private void loadPersona(Component parent, Persona persona) { + String examiner; + String creationDate; + String comment; String name; Collection accounts; Collection metadata; Collection aliases; Collection cases; try { + examiner = persona.getExaminer().getLoginName(); + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); + Date cDate = new Date(persona.getCreatedDate()); + creationDate = dateFormat.format(cDate); + + comment = persona.getComment(); name = persona.getName(); accounts = persona.getPersonaAccounts(); metadata = persona.getMetadata(); @@ -551,6 +681,9 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { return; } this.currentPersona = persona; + this.examinerField.setText(examiner); + this.creationDateField.setText(creationDate); + this.commentField.setText(comment); this.nameField.setText(name); this.currentAccounts.addAll(accounts); this.currentMetadata.addAll(metadata); @@ -560,6 +693,9 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { void clear() { currentPersona = null; + examinerField.setText(""); + creationDateField.setText(""); + commentField.setText(""); nameField.setText(mode == PersonaDetailsMode.CREATE ? Persona.getDefaultName() : ""); currentAccounts = new ArrayList<>(); currentMetadata = new ArrayList<>(); @@ -569,17 +705,19 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { metadataToAdd.clear(); aliasesToAdd.clear(); nameField.setEditable(false); + commentField.setEditable(false); initializeFields(); addAccountBtn.setEnabled(false); addMetadataBtn.setEnabled(false); addAliasBtn.setEnabled(false); - addCaseBtn.setEnabled(false); deleteAccountBtn.setEnabled(false); deleteMetadataBtn.setEnabled(false); deleteAliasBtn.setEnabled(false); - deleteCaseBtn.setEnabled(false); + editAccountBtn.setEnabled(false); + editMetadataBtn.setEnabled(false); + editAliasBtn.setEnabled(false); } /** @@ -677,15 +815,37 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { casesTable.setModel(casesModel); } - void enableEditUIComponents() { - nameField.setEditable(true); - addAccountBtn.setEnabled(true); - addMetadataBtn.setEnabled(true); - addAliasBtn.setEnabled(true); - //addCaseBtn.setEnabled(true); //todo + void configureEditComponents(boolean enabled) { + commentField.setEditable(enabled); + nameField.setEditable(enabled); + addAccountBtn.setEnabled(enabled); + addMetadataBtn.setEnabled(enabled); + addAliasBtn.setEnabled(enabled); + + addAccountBtn.setVisible(enabled); + editAccountBtn.setVisible(enabled); + deleteAccountBtn.setVisible(enabled); + addMetadataBtn.setVisible(enabled); + editMetadataBtn.setVisible(enabled); + deleteMetadataBtn.setVisible(enabled); + addAliasBtn.setVisible(enabled); + editAliasBtn.setVisible(enabled); + deleteAliasBtn.setVisible(enabled); } void initializeFields() { + if (mode == PersonaDetailsMode.CREATE) { + try { + CentralRepoExaminer examiner = CentralRepository.getInstance().getOrInsertExaminer(System.getProperty("user.name")); + examinerField.setText(examiner.getLoginName()); + } catch (CentralRepoException e) { + logger.log(Level.SEVERE, "Failed to access central repository", e); + JOptionPane.showMessageDialog(this, + Bundle.PersonaDetailsPanel_CentralRepoErr_msg(), + Bundle.PersonaDetailsPanel_CentralRepoErr_Title(), + JOptionPane.ERROR_MESSAGE); + } + } updateAccountsTable(); updateMetadataTable(); updateAliasesTable(); @@ -697,14 +857,15 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { this.mode = mode; switch (mode) { case CREATE: - enableEditUIComponents(); + configureEditComponents(true); break; case EDIT: loadPersona(parent, persona); - enableEditUIComponents(); + configureEditComponents(true); break; case VIEW: loadPersona(parent, persona); + configureEditComponents(false); break; default: logger.log(Level.WARNING, "Unsupported mode: {0}", mode); @@ -714,12 +875,14 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { } @Messages({ - "PersonaDetailsPanel_NotEnoughAccounts_msg=A persona needs at least one account", + "PersonaDetailsPanel_NotEnoughAccounts_msg=A persona needs at least one account.", "PersonaDetailsPanel_NotEnoughAccounts_Title=Missing account", - "PersonaDetailsPanel_CentralRepoErr_msg=Failure to write to Central Repository", + "PersonaDetailsPanel_CentralRepoErr_msg=Failure to write to Central Repository.", "PersonaDetailsPanel_CentralRepoErr_Title=Central Repository failure", - "PersonaDetailsPanel_EmptyName_msg=Persona name cannot be empty", - "PersonaDetailsPanel_EmptyName_Title=Empty persona name",}) + "PersonaDetailsPanel_EmptyName_msg=Persona name cannot be empty.", + "PersonaDetailsPanel_EmptyName_Title=Empty persona name", + "PersonaDetailsPanel_EmptyComment_msg=Persona comment cannot be empty.", + "PersonaDetailsPanel_EmptyComment_Title=Empty persona comment",}) Persona okHandler() { if (accountsToAdd.size() + currentAccounts.size() < 1) { JOptionPane.showMessageDialog(this, @@ -729,7 +892,14 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { return null; } - if (nameField.getText().isEmpty()) { + if (StringUtils.isBlank(commentField.getText())) { + JOptionPane.showMessageDialog(this, + Bundle.PersonaDetailsPanel_EmptyComment_msg(), + Bundle.PersonaDetailsPanel_EmptyComment_Title(), + JOptionPane.ERROR_MESSAGE); + return null; + } + if (StringUtils.isBlank(nameField.getText())) { JOptionPane.showMessageDialog(this, Bundle.PersonaDetailsPanel_EmptyName_msg(), Bundle.PersonaDetailsPanel_EmptyName_Title(), @@ -743,7 +913,7 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { try { PAccount firstAccount = accountsToAdd.get(0); ret = Persona.createPersonaForAccount(nameField.getText(), - "", Persona.PersonaStatus.ACTIVE, firstAccount.account, + commentField.getText(), Persona.PersonaStatus.ACTIVE, firstAccount.account, firstAccount.justification, firstAccount.confidence); for (int i = 1; i < accountsToAdd.size(); i++) { ret.addAccount(accountsToAdd.get(i).account, @@ -768,6 +938,7 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { case EDIT: try { ret = currentPersona; + currentPersona.setComment(commentField.getText()); currentPersona.setName(nameField.getText()); for (PAccount acc : accountsToAdd) { ret.addAccount(acc.account, acc.justification, acc.confidence); @@ -775,18 +946,27 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { for (PersonaAccount acc : accountsToRemove) { ret.removeAccount(acc); } + for (HashMap.Entry entry : accountsToEdit.entrySet()) { + ret.modifyAccount(entry.getKey(), entry.getValue().confidence, entry.getValue().justification); + } for (PMetadata md : metadataToAdd) { ret.addMetadata(md.name, md.value, md.justification, md.confidence); } for (PersonaMetadata md : metadataToRemove) { ret.removeMetadata(md); } + for (HashMap.Entry entry : metadataToEdit.entrySet()) { + ret.modifyMetadata(entry.getKey(), entry.getValue().confidence, entry.getValue().justification); + } for (PAlias pa : aliasesToAdd) { ret.addAlias(pa.alias, pa.justification, pa.confidence); } for (PersonaAlias pa : aliasesToRemove) { ret.removeAlias(pa); } + for (HashMap.Entry entry : aliasesToEdit.entrySet()) { + ret.modifyAlias(entry.getKey(), entry.getValue().confidence, entry.getValue().justification); + } } catch (CentralRepoException e) { logger.log(Level.SEVERE, "Failed to access central repository", e); JOptionPane.showMessageDialog(this, @@ -797,10 +977,21 @@ public final class PersonaDetailsPanel extends javax.swing.JPanel { } break; case VIEW: + ret = currentPersona; break; default: logger.log(Level.SEVERE, "Unsupported mode: {0}", mode); } return ret; } + + /** + * Sets the persona name field. + * + * @param name Persona name. + */ + public void setPersonaName(String name) { + nameField.setText(name); + } + } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddMetadataDialog.form b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaMetadataDialog.form similarity index 87% rename from Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddMetadataDialog.form rename to Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaMetadataDialog.form index b856f6a516..e2b06c0318 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddMetadataDialog.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaMetadataDialog.form @@ -103,16 +103,16 @@ - - - - - + + + + + @@ -121,49 +121,35 @@ - + - + - + - - - - - - - - - - - - - - - + - + @@ -177,12 +163,26 @@ + + + + + + + + + + + + + + - + @@ -201,7 +201,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddMetadataDialog.java b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaMetadataDialog.java similarity index 75% rename from Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddMetadataDialog.java rename to Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaMetadataDialog.java index c09c0ee5c5..15fad56114 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/AddMetadataDialog.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaMetadataDialog.java @@ -19,8 +19,9 @@ package org.sleuthkit.autopsy.centralrepository.persona; import javax.swing.JDialog; -import javax.swing.JFrame; import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; +import org.apache.commons.lang3.StringUtils; import org.openide.util.NbBundle.Messages; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.centralrepository.datamodel.Persona; @@ -29,26 +30,47 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.Persona; * Configuration dialog for adding metadata to a persona. */ @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives -public class AddMetadataDialog extends JDialog { +public class PersonaMetadataDialog extends JDialog { private static final long serialVersionUID = 1L; private final PersonaDetailsPanel pdp; + private PersonaDetailsPanel.PMetadata currentMetadata; + /** * Creates new add metadata dialog */ @Messages({"AddMetadataDialog.title.text=Add Metadata",}) - public AddMetadataDialog(PersonaDetailsPanel pdp) { - super((JFrame) WindowManager.getDefault().getMainWindow(), + public PersonaMetadataDialog(PersonaDetailsPanel pdp) { + super(SwingUtilities.windowForComponent(pdp), Bundle.AddMetadataDialog_title_text(), - true); + ModalityType.APPLICATION_MODAL); this.pdp = pdp; initComponents(); display(); } + PersonaMetadataDialog(PersonaDetailsPanel pdp, PersonaDetailsPanel.PMetadata md) { + super(SwingUtilities.windowForComponent(pdp), + Bundle.AddMetadataDialog_title_text(), + ModalityType.APPLICATION_MODAL); + this.pdp = pdp; + + initComponents(); + currentMetadata = md; + confidenceComboBox.setSelectedItem(md.confidence); + justificationTextField.setText(md.justification); + nameTextField.setText(md.name); + valueTextField.setText(md.value); + + nameTextField.setEnabled(false); + valueTextField.setEnabled(false); + + display(); + } + /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always @@ -63,10 +85,10 @@ public class AddMetadataDialog extends JDialog { nameTextField = new javax.swing.JTextField(); valueLbl = new javax.swing.JLabel(); valueTextField = new javax.swing.JTextField(); - justificationLbl = new javax.swing.JLabel(); - justificationTextField = new javax.swing.JTextField(); confidenceLbl = new javax.swing.JLabel(); confidenceComboBox = new javax.swing.JComboBox<>(); + justificationLbl = new javax.swing.JLabel(); + justificationTextField = new javax.swing.JTextField(); cancelBtn = new javax.swing.JButton(); okBtn = new javax.swing.JButton(); @@ -75,22 +97,22 @@ public class AddMetadataDialog extends JDialog { settingsPanel.setBorder(javax.swing.BorderFactory.createEtchedBorder()); - org.openide.awt.Mnemonics.setLocalizedText(nameLbl, org.openide.util.NbBundle.getMessage(AddMetadataDialog.class, "AddMetadataDialog.nameLbl.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(nameLbl, org.openide.util.NbBundle.getMessage(PersonaMetadataDialog.class, "PersonaMetadataDialog.nameLbl.text")); // NOI18N - nameTextField.setText(org.openide.util.NbBundle.getMessage(AddMetadataDialog.class, "AddMetadataDialog.nameTextField.text")); // NOI18N + nameTextField.setText(org.openide.util.NbBundle.getMessage(PersonaMetadataDialog.class, "PersonaMetadataDialog.nameTextField.text")); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(valueLbl, org.openide.util.NbBundle.getMessage(AddMetadataDialog.class, "AddMetadataDialog.valueLbl.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(valueLbl, org.openide.util.NbBundle.getMessage(PersonaMetadataDialog.class, "PersonaMetadataDialog.valueLbl.text")); // NOI18N - valueTextField.setText(org.openide.util.NbBundle.getMessage(AddMetadataDialog.class, "AddMetadataDialog.valueTextField.text")); // NOI18N + valueTextField.setText(org.openide.util.NbBundle.getMessage(PersonaMetadataDialog.class, "PersonaMetadataDialog.valueTextField.text")); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(justificationLbl, org.openide.util.NbBundle.getMessage(AddMetadataDialog.class, "AddMetadataDialog.justificationLbl.text")); // NOI18N - - justificationTextField.setText(org.openide.util.NbBundle.getMessage(AddMetadataDialog.class, "AddMetadataDialog.justificationTextField.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(confidenceLbl, org.openide.util.NbBundle.getMessage(AddMetadataDialog.class, "AddMetadataDialog.confidenceLbl.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(confidenceLbl, org.openide.util.NbBundle.getMessage(PersonaMetadataDialog.class, "PersonaMetadataDialog.confidenceLbl.text")); // NOI18N confidenceComboBox.setModel(new javax.swing.DefaultComboBoxModel<>(org.sleuthkit.autopsy.centralrepository.datamodel.Persona.Confidence.values())); + org.openide.awt.Mnemonics.setLocalizedText(justificationLbl, org.openide.util.NbBundle.getMessage(PersonaMetadataDialog.class, "PersonaMetadataDialog.justificationLbl.text")); // NOI18N + + justificationTextField.setText(org.openide.util.NbBundle.getMessage(PersonaMetadataDialog.class, "PersonaMetadataDialog.justificationTextField.text")); // NOI18N + javax.swing.GroupLayout settingsPanelLayout = new javax.swing.GroupLayout(settingsPanel); settingsPanel.setLayout(settingsPanelLayout); settingsPanelLayout.setHorizontalGroup( @@ -128,17 +150,17 @@ public class AddMetadataDialog extends JDialog { .addComponent(valueLbl) .addComponent(valueTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(settingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(justificationTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(justificationLbl)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addGroup(settingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(confidenceComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(confidenceLbl)) - .addContainerGap()) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(settingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(justificationTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(justificationLbl)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); - org.openide.awt.Mnemonics.setLocalizedText(cancelBtn, org.openide.util.NbBundle.getMessage(AddMetadataDialog.class, "AddMetadataDialog.cancelBtn.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(cancelBtn, org.openide.util.NbBundle.getMessage(PersonaMetadataDialog.class, "PersonaMetadataDialog.cancelBtn.text")); // NOI18N cancelBtn.setMaximumSize(new java.awt.Dimension(79, 23)); cancelBtn.setMinimumSize(new java.awt.Dimension(79, 23)); cancelBtn.setPreferredSize(new java.awt.Dimension(79, 23)); @@ -148,7 +170,7 @@ public class AddMetadataDialog extends JDialog { } }); - org.openide.awt.Mnemonics.setLocalizedText(okBtn, org.openide.util.NbBundle.getMessage(AddMetadataDialog.class, "AddMetadataDialog.okBtn.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(okBtn, org.openide.util.NbBundle.getMessage(PersonaMetadataDialog.class, "PersonaMetadataDialog.okBtn.text")); // NOI18N okBtn.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { okBtnActionPerformed(evt); @@ -191,19 +213,41 @@ public class AddMetadataDialog extends JDialog { @Messages({ "AddMetadataDialog_dup_Title=Metadata add failure", - "AddMetadataDialog_dup_msg=A metadata entry with this name has already been added to this persona",}) + "AddMetadataDialog_dup_msg=A metadata entry with this name has already been added to this persona.", + "AddMetadataDialog_empty_name_Title=Missing field(s)", + "AddMetadataDialog_empty_name_msg=A metadata entry cannot have an empty name or value.",}) private void okBtnActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okBtnActionPerformed - if (pdp.addMetadata( - nameTextField.getText(), - valueTextField.getText(), - justificationTextField.getText(), - (Persona.Confidence) confidenceComboBox.getSelectedItem())) { + if (StringUtils.isBlank(nameTextField.getText()) || StringUtils.isBlank(valueTextField.getText())) { + JOptionPane.showMessageDialog(this, + Bundle.AddMetadataDialog_empty_name_msg(), + Bundle.AddMetadataDialog_empty_name_Title(), + JOptionPane.ERROR_MESSAGE); + return; + } + if (StringUtils.isBlank(justificationTextField.getText())) { + JOptionPane.showMessageDialog(this, + Bundle.PersonaDetailsPanel_empty_justification_msg(), + Bundle.PersonaDetailsPanel_empty_justification_Title(), + JOptionPane.ERROR_MESSAGE); + return; + } + + Persona.Confidence confidence = (Persona.Confidence) confidenceComboBox.getSelectedItem(); + String justification = justificationTextField.getText(); + + if (currentMetadata != null) { + currentMetadata.confidence = confidence; + currentMetadata.justification = justification; dispose(); } else { - JOptionPane.showMessageDialog(this, + if (pdp.addMetadata(nameTextField.getText(), valueTextField.getText(), justification, confidence)) { + dispose(); + } else { + JOptionPane.showMessageDialog(this, Bundle.AddMetadataDialog_dup_msg(), Bundle.AddMetadataDialog_dup_Title(), JOptionPane.ERROR_MESSAGE); + } } }//GEN-LAST:event_okBtnActionPerformed diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaManagerTopComponent.form b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonasTopComponent.form similarity index 59% rename from Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaManagerTopComponent.form rename to Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonasTopComponent.form index 918e5e4302..9df2d2ba55 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaManagerTopComponent.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonasTopComponent.form @@ -1,35 +1,12 @@ - -
- - - + @@ -46,17 +23,52 @@ - + + - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -73,13 +85,7 @@ - - - - - - - + @@ -89,6 +95,20 @@ + + + + + + + + + + + + + + @@ -97,7 +117,8 @@ - + + @@ -106,7 +127,7 @@ - + @@ -114,6 +135,10 @@ + + + + @@ -122,7 +147,7 @@ - + @@ -133,7 +158,7 @@ - + @@ -143,9 +168,15 @@ - + + + + + + + + - @@ -158,20 +189,20 @@ - + - <ResourceString bundle="org/sleuthkit/autopsy/centralrepository/persona/Bundle.properties" key="PersonaManagerTopComponent.resultsTable.columnModel.title0" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + <ResourceString bundle="org/sleuthkit/autopsy/centralrepository/persona/Bundle.properties" key="PersonasTopComponent.resultsTable.columnModel.title0" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - <ResourceString bundle="org/sleuthkit/autopsy/centralrepository/persona/Bundle.properties" key="PersonaManagerTopComponent.resultsTable.columnModel.title1" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + <ResourceString bundle="org/sleuthkit/autopsy/centralrepository/persona/Bundle.properties" key="PersonasTopComponent.resultsTable.columnModel.title1" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> @@ -185,48 +216,69 @@ - + - - - - - - - - - - - - - - - - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaManagerTopComponent.java b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonasTopComponent.java similarity index 51% rename from Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaManagerTopComponent.java rename to Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonasTopComponent.java index c2c7476496..e051529f11 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonaManagerTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonasTopComponent.java @@ -20,6 +20,8 @@ package org.sleuthkit.autopsy.centralrepository.persona; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -27,14 +29,19 @@ import java.util.logging.Level; import javax.swing.JOptionPane; import javax.swing.JTable; import javax.swing.ListSelectionModel; +import javax.swing.event.AncestorListener; +import javax.swing.event.AncestorEvent; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableModel; +import org.openide.DialogDisplayer; +import org.openide.NotifyDescriptor; import org.openide.util.NbBundle.Messages; import org.openide.windows.RetainLocation; import org.openide.windows.TopComponent; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; import org.sleuthkit.autopsy.centralrepository.datamodel.Persona; import org.sleuthkit.autopsy.coreutils.Logger; @@ -42,26 +49,28 @@ import org.sleuthkit.autopsy.coreutils.Logger; * Top component for the Personas tool * */ -@TopComponent.Description(preferredID = "PersonaManagerTopComponent", persistenceType = TopComponent.PERSISTENCE_NEVER) -@TopComponent.Registration(mode = "personamanager", openAtStartup = false) -@RetainLocation("personamanager") +@TopComponent.Description(preferredID = "PersonasTopComponent", persistenceType = TopComponent.PERSISTENCE_NEVER) +@TopComponent.Registration(mode = "personas", openAtStartup = false) +@RetainLocation("personas") @SuppressWarnings("PMD.SingularField") -public final class PersonaManagerTopComponent extends TopComponent { +public final class PersonasTopComponent extends TopComponent { - private static final Logger logger = Logger.getLogger(PersonaManagerTopComponent.class.getName()); + private static final long serialVersionUID = 1L; + + private static final Logger logger = Logger.getLogger(PersonasTopComponent.class.getName()); private List currentResults = null; private Persona selectedPersona = null; @Messages({ - "PMTopComponent_Name=Persona Manager", - "PMTopComponent_delete_exception_Title=Delete failure", - "PMTopComponent_delete_exception_msg=Failed to delete persona", - }) - public PersonaManagerTopComponent() { + "PersonasTopComponent_Name=Personas", + "PersonasTopComponent_delete_exception_Title=Delete failure", + "PersonasTopComponent_delete_exception_msg=Failed to delete persona.", + "PersonasTopComponent_delete_confirmation_Title=Are you sure?", + "PersonasTopComponent_delete_confirmation_msg=Are you sure you want to delete this persona?",}) + public PersonasTopComponent() { initComponents(); - setName(Bundle.PMTopComponent_Name()); - executeSearch(); + setName(Bundle.PersonasTopComponent_Name()); searchBtn.addActionListener(new ActionListener() { @Override @@ -73,7 +82,7 @@ public final class PersonaManagerTopComponent extends TopComponent { editBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - new PersonaDetailsDialog(PersonaManagerTopComponent.this, + new PersonaDetailsDialog(PersonasTopComponent.this, PersonaDetailsMode.EDIT, selectedPersona, new CreateEditCallbackImpl()); } }); @@ -81,27 +90,34 @@ public final class PersonaManagerTopComponent extends TopComponent { createBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - new PersonaDetailsDialog(PersonaManagerTopComponent.this, + new PersonaDetailsDialog(PersonasTopComponent.this, PersonaDetailsMode.CREATE, selectedPersona, new CreateEditCallbackImpl()); } }); - + deleteBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - try { - if (selectedPersona != null) { - selectedPersona.delete(); + NotifyDescriptor confirm = new NotifyDescriptor.Confirmation( + Bundle.PersonasTopComponent_delete_confirmation_msg(), + Bundle.PersonasTopComponent_delete_confirmation_Title(), + NotifyDescriptor.YES_NO_OPTION); + DialogDisplayer.getDefault().notify(confirm); + if (confirm.getValue().equals(NotifyDescriptor.YES_OPTION)) { + try { + if (selectedPersona != null) { + selectedPersona.delete(); + } + } catch (CentralRepoException ex) { + logger.log(Level.SEVERE, "Failed to delete persona: " + selectedPersona.getName(), ex); + JOptionPane.showMessageDialog(PersonasTopComponent.this, + Bundle.PersonasTopComponent_delete_exception_msg(), + Bundle.PersonasTopComponent_delete_exception_Title(), + JOptionPane.ERROR_MESSAGE); + return; } - } catch (CentralRepoException ex) { - logger.log(Level.SEVERE, "Failed to delete persona: " + selectedPersona.getName(), ex); - JOptionPane.showMessageDialog(PersonaManagerTopComponent.this, - Bundle.PMTopComponent_delete_exception_msg(), - Bundle.PMTopComponent_delete_exception_Title(), - JOptionPane.ERROR_MESSAGE); - return; + executeSearch(); } - executeSearch(); } }); @@ -113,6 +129,33 @@ public final class PersonaManagerTopComponent extends TopComponent { handleSelectionChange(e); } }); + + searchNameRadio.addActionListener((ActionEvent e) -> { + searchField.setText(""); + }); + + searchAccountRadio.addActionListener((ActionEvent e) -> { + searchField.setText(""); + }); + + createAccountBtn.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + new CreatePersonaAccountDialog(detailsPanel); + } + }); + + /** + * Listens for when this component will be rendered and executes a + * search to update gui when it is displayed. + */ + addComponentListener(new ComponentAdapter() { + @Override + public void componentShown(ComponentEvent e) { + resetSearchControls(); + setKeywordSearchEnabled(false, true); + } + }); } /** @@ -133,6 +176,34 @@ public final class PersonaManagerTopComponent extends TopComponent { } } + /** + * Resets search controls to default state. + */ + private void resetSearchControls() { + searchField.setText(""); + searchNameRadio.setSelected(true); + searchAccountRadio.setSelected(false); + } + + /** + * Sets up the GUI for appropriate state for keyword search enabled state. + * + * @param selected Whether or not keyword search is enabled. + * @param setFilterCb Whether or not the filter checkbox should be + * manipulated as a part of this change. + */ + private void setKeywordSearchEnabled(boolean selected, boolean setFilterCb) { + if (setFilterCb && cbFilterByKeyword.isSelected() != selected) { + cbFilterByKeyword.setSelected(selected); + } + + searchField.setEnabled(selected); + searchNameRadio.setEnabled(selected); + searchAccountRadio.setEnabled(selected); + + executeSearch(); + } + void setPersona(int index) { Persona persona = currentResults.get(index); selectedPersona = persona; @@ -195,17 +266,36 @@ public final class PersonaManagerTopComponent extends TopComponent { } @Messages({ - "PMTopComponent_search_exception_Title=Search failure", - "PMTopComponent_search_exception_msg=Failed to search personas",}) + "PersonasTopComponent_search_exception_Title=There was a failure during the search. Try opening a case to fully initialize the central repository database.", + "PersonasTopComponent_search_exception_msg=Failed to search personas.", + "PersonasTopComponent_noCR_msg=Central Repository is not enabled.",}) private void executeSearch() { + // To prevent downstream failures, only execute search if central repository is enabled + if (!CentralRepository.isEnabled()) { + logger.log(Level.SEVERE, "Central Repository is not enabled, but execute search was called."); + JOptionPane.showMessageDialog(this, + Bundle.PersonasTopComponent_search_exception_Title(), + Bundle.PersonasTopComponent_noCR_msg(), + JOptionPane.ERROR_MESSAGE); + return; + } + Collection results; try { - results = Persona.getPersonaByName(searchField.getText()); + if (cbFilterByKeyword.isSelected()) { + if (searchNameRadio.isSelected()) { + results = Persona.getPersonaByName(searchField.getText()); + } else { + results = Persona.getPersonaByAccountIdentifierLike(searchField.getText()); + } + } else { + results = Persona.getPersonaByName(""); + } } catch (CentralRepoException ex) { logger.log(Level.SEVERE, "Failed to search personas", ex); JOptionPane.showMessageDialog(this, - Bundle.PMTopComponent_search_exception_Title(), - Bundle.PMTopComponent_search_exception_msg(), + Bundle.PersonasTopComponent_search_exception_Title(), + Bundle.PersonasTopComponent_search_exception_msg(), JOptionPane.ERROR_MESSAGE); return; } @@ -231,50 +321,77 @@ public final class PersonaManagerTopComponent extends TopComponent { private void initComponents() { searchButtonGroup = new javax.swing.ButtonGroup(); - jSplitPane1 = new javax.swing.JSplitPane(); + introTextScrollPane = new javax.swing.JScrollPane(); + introText = new javax.swing.JTextArea(); + mainSplitPane = new javax.swing.JSplitPane(); searchPanel = new javax.swing.JPanel(); searchField = new javax.swing.JTextField(); searchNameRadio = new javax.swing.JRadioButton(); searchAccountRadio = new javax.swing.JRadioButton(); + searchBtn = new javax.swing.JButton(); resultsPane = new javax.swing.JScrollPane(); resultsTable = new javax.swing.JTable(); - searchBtn = new javax.swing.JButton(); + createAccountBtn = new javax.swing.JButton(); editBtn = new javax.swing.JButton(); - createBtn = new javax.swing.JButton(); deleteBtn = new javax.swing.JButton(); + createButtonSeparator = new javax.swing.JSeparator(); + createBtn = new javax.swing.JButton(); + cbFilterByKeyword = new javax.swing.JCheckBox(); + detailsScrollPane = new javax.swing.JScrollPane(); detailsPanel = new org.sleuthkit.autopsy.centralrepository.persona.PersonaDetailsPanel(); - setMinimumSize(new java.awt.Dimension(400, 400)); + setName(""); // NOI18N - searchField.setText(org.openide.util.NbBundle.getMessage(PersonaManagerTopComponent.class, "PersonaManagerTopComponent.searchField.text")); // NOI18N + introTextScrollPane.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); + + introText.setBackground(getBackground()); + introText.setColumns(20); + introText.setLineWrap(true); + introText.setRows(5); + introText.setText(org.openide.util.NbBundle.getMessage(PersonasTopComponent.class, "PersonasTopComponent.introText.text")); // NOI18N + introText.setWrapStyleWord(true); + introText.setFocusable(false); + introTextScrollPane.setViewportView(introText); + + mainSplitPane.setDividerLocation(400); + + searchField.setText(org.openide.util.NbBundle.getMessage(PersonasTopComponent.class, "PersonasTopComponent.searchField.text")); // NOI18N searchButtonGroup.add(searchNameRadio); searchNameRadio.setSelected(true); - org.openide.awt.Mnemonics.setLocalizedText(searchNameRadio, org.openide.util.NbBundle.getMessage(PersonaManagerTopComponent.class, "PersonaManagerTopComponent.searchNameRadio.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(searchNameRadio, org.openide.util.NbBundle.getMessage(PersonasTopComponent.class, "PersonasTopComponent.searchNameRadio.text")); // NOI18N searchButtonGroup.add(searchAccountRadio); - org.openide.awt.Mnemonics.setLocalizedText(searchAccountRadio, org.openide.util.NbBundle.getMessage(PersonaManagerTopComponent.class, "PersonaManagerTopComponent.searchAccountRadio.text")); // NOI18N - searchAccountRadio.setEnabled(false); + org.openide.awt.Mnemonics.setLocalizedText(searchAccountRadio, org.openide.util.NbBundle.getMessage(PersonasTopComponent.class, "PersonasTopComponent.searchAccountRadio.text")); // NOI18N - resultsTable.setToolTipText(org.openide.util.NbBundle.getMessage(PersonaManagerTopComponent.class, "PersonaManagerTopComponent.resultsTable.toolTipText")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(searchBtn, org.openide.util.NbBundle.getMessage(PersonasTopComponent.class, "PersonasTopComponent.searchBtn.text")); // NOI18N + + resultsTable.setToolTipText(org.openide.util.NbBundle.getMessage(PersonasTopComponent.class, "PersonasTopComponent.resultsTable.toolTipText")); // NOI18N resultsTable.getTableHeader().setReorderingAllowed(false); resultsPane.setViewportView(resultsTable); if (resultsTable.getColumnModel().getColumnCount() > 0) { resultsTable.getColumnModel().getColumn(0).setMaxWidth(25); - resultsTable.getColumnModel().getColumn(0).setHeaderValue(org.openide.util.NbBundle.getMessage(PersonaManagerTopComponent.class, "PersonaManagerTopComponent.resultsTable.columnModel.title0")); // NOI18N - resultsTable.getColumnModel().getColumn(1).setHeaderValue(org.openide.util.NbBundle.getMessage(PersonaManagerTopComponent.class, "PersonaManagerTopComponent.resultsTable.columnModel.title1")); // NOI18N + resultsTable.getColumnModel().getColumn(0).setHeaderValue(org.openide.util.NbBundle.getMessage(PersonasTopComponent.class, "PersonasTopComponent.resultsTable.columnModel.title0")); // NOI18N + resultsTable.getColumnModel().getColumn(1).setHeaderValue(org.openide.util.NbBundle.getMessage(PersonasTopComponent.class, "PersonasTopComponent.resultsTable.columnModel.title1")); // NOI18N } - org.openide.awt.Mnemonics.setLocalizedText(searchBtn, org.openide.util.NbBundle.getMessage(PersonaManagerTopComponent.class, "PersonaManagerTopComponent.searchBtn.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(createAccountBtn, org.openide.util.NbBundle.getMessage(PersonasTopComponent.class, "PersonasTopComponent.createAccountBtn.text")); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(editBtn, org.openide.util.NbBundle.getMessage(PersonaManagerTopComponent.class, "PersonaManagerTopComponent.editBtn.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(editBtn, org.openide.util.NbBundle.getMessage(PersonasTopComponent.class, "PersonasTopComponent.editBtn.text")); // NOI18N editBtn.setEnabled(false); - org.openide.awt.Mnemonics.setLocalizedText(createBtn, org.openide.util.NbBundle.getMessage(PersonaManagerTopComponent.class, "PersonaManagerTopComponent.createBtn.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(deleteBtn, org.openide.util.NbBundle.getMessage(PersonaManagerTopComponent.class, "PersonaManagerTopComponent.deleteBtn.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(deleteBtn, org.openide.util.NbBundle.getMessage(PersonasTopComponent.class, "PersonasTopComponent.deleteBtn.text")); // NOI18N deleteBtn.setEnabled(false); + org.openide.awt.Mnemonics.setLocalizedText(createBtn, org.openide.util.NbBundle.getMessage(PersonasTopComponent.class, "PersonasTopComponent.createBtn.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(cbFilterByKeyword, org.openide.util.NbBundle.getMessage(PersonasTopComponent.class, "PersonasTopComponent.cbFilterByKeyword.text")); // NOI18N + cbFilterByKeyword.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cbFilterByKeywordActionPerformed(evt); + } + }); + javax.swing.GroupLayout searchPanelLayout = new javax.swing.GroupLayout(searchPanel); searchPanel.setLayout(searchPanelLayout); searchPanelLayout.setHorizontalGroup( @@ -282,12 +399,7 @@ public final class PersonaManagerTopComponent extends TopComponent { .addGroup(searchPanelLayout.createSequentialGroup() .addContainerGap() .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(searchPanelLayout.createSequentialGroup() - .addComponent(createBtn) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(editBtn) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(deleteBtn)) + .addComponent(createButtonSeparator) .addComponent(resultsPane, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) .addComponent(searchField) .addGroup(searchPanelLayout.createSequentialGroup() @@ -295,13 +407,25 @@ public final class PersonaManagerTopComponent extends TopComponent { .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(searchAccountRadio) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(searchBtn))) + .addComponent(searchBtn)) + .addGroup(searchPanelLayout.createSequentialGroup() + .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(createAccountBtn) + .addGroup(searchPanelLayout.createSequentialGroup() + .addComponent(createBtn) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(editBtn) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(deleteBtn)) + .addComponent(cbFilterByKeyword)) + .addGap(0, 50, Short.MAX_VALUE))) .addContainerGap()) ); searchPanelLayout.setVerticalGroup( searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(searchPanelLayout.createSequentialGroup() - .addContainerGap() + .addComponent(cbFilterByKeyword) + .addGap(1, 1, 1) .addComponent(searchField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) @@ -309,36 +433,57 @@ public final class PersonaManagerTopComponent extends TopComponent { .addComponent(searchAccountRadio) .addComponent(searchBtn)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(resultsPane, javax.swing.GroupLayout.DEFAULT_SIZE, 528, Short.MAX_VALUE) + .addComponent(resultsPane, javax.swing.GroupLayout.PREFERRED_SIZE, 302, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(editBtn) .addComponent(createBtn) .addComponent(deleteBtn)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(createButtonSeparator, javax.swing.GroupLayout.PREFERRED_SIZE, 4, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(createAccountBtn, javax.swing.GroupLayout.PREFERRED_SIZE, 32, javax.swing.GroupLayout.PREFERRED_SIZE) .addContainerGap()) ); - jSplitPane1.setLeftComponent(searchPanel); - jSplitPane1.setRightComponent(detailsPanel); + mainSplitPane.setLeftComponent(searchPanel); + + detailsScrollPane.setViewportView(detailsPanel); + + mainSplitPane.setRightComponent(detailsScrollPane); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jSplitPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 692, Short.MAX_VALUE) + .addComponent(introTextScrollPane) + .addComponent(mainSplitPane, javax.swing.GroupLayout.DEFAULT_SIZE, 724, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jSplitPane1) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addComponent(introTextScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 49, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(mainSplitPane, javax.swing.GroupLayout.DEFAULT_SIZE, 489, Short.MAX_VALUE)) ); }// //GEN-END:initComponents + private void cbFilterByKeywordActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cbFilterByKeywordActionPerformed + setKeywordSearchEnabled(cbFilterByKeyword.isSelected(), false); + }//GEN-LAST:event_cbFilterByKeywordActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JCheckBox cbFilterByKeyword; + private javax.swing.JButton createAccountBtn; private javax.swing.JButton createBtn; + private javax.swing.JSeparator createButtonSeparator; private javax.swing.JButton deleteBtn; private org.sleuthkit.autopsy.centralrepository.persona.PersonaDetailsPanel detailsPanel; + private javax.swing.JScrollPane detailsScrollPane; private javax.swing.JButton editBtn; - private javax.swing.JSplitPane jSplitPane1; + private javax.swing.JTextArea introText; + private javax.swing.JScrollPane introTextScrollPane; + private javax.swing.JSplitPane mainSplitPane; private javax.swing.JScrollPane resultsPane; private javax.swing.JTable resultsTable; private javax.swing.JRadioButton searchAccountRadio; diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributePanel.java b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributePanel.java index b301e69d04..7bfdd2ad02 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributePanel.java +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributePanel.java @@ -862,7 +862,7 @@ final class CommonAttributePanel extends javax.swing.JDialog implements Observer /** * If the settings reflect that a inter-case search is being performed, * checks that the data sources in the current case have been processed with - * Correlation Engine enabled and exist in the central repository. Prompting + * Central Repository enabled and exist in the central repository. Prompting * the user as to whether they still want to perform the search in the case * any data sources are unprocessed. If the settings reflect that a * intra-case search is being performed, it just performs the search. @@ -870,7 +870,7 @@ final class CommonAttributePanel extends javax.swing.JDialog implements Observer * Notes: - Does not check that the data sources were processed into the * current central repository instead of another. - Does not check that the * appropriate modules to make all correlation types available were run. - - * Does not check if the correlation engine was run with any of the + * Does not check if the Central Repository was run with any of the * correlation properties properties disabled. */ @Messages({"CommonAttributePanel.incompleteResults.introText=Results may be incomplete. Not all data sources in the current case were ingested into the current Central Repository. The following data sources have not been processed:", @@ -902,14 +902,14 @@ final class CommonAttributePanel extends javax.swing.JDialog implements Observer //if the datasource was previously processed we do not need to perform this check for (CorrelationDataSource correlatedDataSource : correlatedDataSources) { if (deviceID.equals(correlatedDataSource.getDeviceID())) { - //if the datasource exists in the central repository it may of been processed with the correlation engine + //if the datasource exists in the central repository it may of been processed with the Central Repository dataSourceCorrelationMap.put(dataSource, CorrelatedStatus.IN_CENTRAL_REPO); break; } } } if (dataSourceCorrelationMap.get(dataSource) == CorrelatedStatus.IN_CENTRAL_REPO) { - //if the data source was in the central repository check if any of the modules run on it were the correlation engine + //if the data source was in the central repository check if any of the modules run on it were the Central Repository for (IngestModuleInfo ingestModuleInfo : jobInfo.getIngestModuleInfo()) { if (correlationEngineModuleName.equals(ingestModuleInfo.getDisplayName())) { dataSourceCorrelationMap.put(dataSource, CorrelatedStatus.CORRELATED); diff --git a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties index 747f0c82f7..9a9ebd3929 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties @@ -14,7 +14,7 @@ FiltersPanel.endCheckBox.text=End: FiltersPanel.refreshButton.text=Refresh FiltersPanel.deviceRequiredLabel.text=Select at least one. FiltersPanel.accountTypeRequiredLabel.text=Select at least one. -FiltersPanel.needsRefreshLabel.text=Displayed data is out of date. Press Refresh. +FiltersPanel.needsRefreshLabel.text=Displayed data may be out of date. Press Refresh to update. VisualizationPanel.jButton1.text=Fast Organic CVTTopComponent.vizPanel.TabConstraints.tabTitle=Visualize CVTTopComponent.accountsBrowser.TabConstraints.tabTitle_1=Browse diff --git a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties-MERGED index 65c130bd6e..0d4db75372 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties-MERGED @@ -1,3 +1,8 @@ +# {0} - PersonaAccount count +AccountInstanceNode_Tooltip_suffix=(1 of {0}) +# {0} - Contact Name +# {1} - Persona Name +AccountInstanceNode_Tooltip_Template=Contact: {0} - Persona: {1} AccountNode.accountName=Account AccountNode.accountType=Type AccountNode.device=Device @@ -21,7 +26,7 @@ FiltersPanel.endCheckBox.text=End: FiltersPanel.refreshButton.text=Refresh FiltersPanel.deviceRequiredLabel.text=Select at least one. FiltersPanel.accountTypeRequiredLabel.text=Select at least one. -FiltersPanel.needsRefreshLabel.text=Displayed data is out of date. Press Refresh. +FiltersPanel.needsRefreshLabel.text=Displayed data may be out of date. Press Refresh to update. OpenCVTAction.displayName=Communications PinAccountsAction.pluralText=Add Selected Accounts to Visualization PinAccountsAction.singularText=Add Selected Account to Visualization diff --git a/Core/src/org/sleuthkit/autopsy/communications/CVTFilterRefresher.java b/Core/src/org/sleuthkit/autopsy/communications/CVTFilterRefresher.java new file mode 100755 index 0000000000..b5560ab825 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/CVTFilterRefresher.java @@ -0,0 +1,163 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit 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.communications; + +import java.beans.PropertyChangeEvent; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.SwingUtilities; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.guiutils.RefreshThrottler; +import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.DATA_ADDED; +import org.sleuthkit.autopsy.ingest.ModuleDataEvent; +import org.sleuthkit.datamodel.Account; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Refreshes the CVTFilterPanel. + */ +abstract class CVTFilterRefresher implements RefreshThrottler.Refresher { + + private static final Logger logger = Logger.getLogger(CVTFilterRefresher.class.getName()); + /** + * contains all of the gui control specific update code. Refresh will call + * this method with an involkLater so that the updating of the swing + * controls can happen on the EDT. + * + * @param data + */ + abstract void updateFilterPanel(FilterPanelData data); + + @Override + public void refresh() { + try { + Integer startTime; + Integer endTime; + SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase(); + + // Fetch Min/Max start times + try (SleuthkitCase.CaseDbQuery dbQuery = skCase.executeQuery("SELECT MAX(date_time) as end, MIN(date_time) as start from account_relationships")) { + // ResultSet is closed by CasDBQuery + ResultSet rs = dbQuery.getResultSet(); + rs.next(); + startTime = rs.getInt("start"); // NON-NLS + endTime = rs.getInt("end"); // NON-NLS + + } + // Get the devices with CVT artifacts + List deviceObjIds = new ArrayList<>(); + try (SleuthkitCase.CaseDbQuery queryResult = skCase.executeQuery("SELECT DISTINCT data_source_obj_id FROM account_relationships")) { + // ResultSet is closed by CasDBQuery + ResultSet rs = queryResult.getResultSet(); + while (rs.next()) { + deviceObjIds.add(rs.getInt(1)); + } + } + + // The map key is the Content name instead of the data source name + // to match how the CVT filters work. + Map dataSourceMap = new HashMap<>(); + for (DataSource dataSource : skCase.getDataSources()) { + if (deviceObjIds.contains((int) dataSource.getId())) { + String dsName = skCase.getContentById(dataSource.getId()).getName(); + dataSourceMap.put(dsName, dataSource); + } + } + + List accountTypesInUse = skCase.getCommunicationsManager().getAccountTypesInUse(); + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + updateFilterPanel(new FilterPanelData(dataSourceMap, accountTypesInUse, startTime, endTime)); + } + }); + + } catch (SQLException | TskCoreException ex) { + logger.log(Level.WARNING, "Unable to update CVT filter panel.", ex); + } catch (NoCurrentCaseException notUsed) { + /** + * Case is closed, do nothing. + */ + } + + } + + @Override + public boolean isRefreshRequired(PropertyChangeEvent evt) { + String eventType = evt.getPropertyName(); + if (eventType.equals(DATA_ADDED.toString())) { + // Indicate that a refresh may be needed, unless the data added is Keyword or Hashset hits + ModuleDataEvent eventData = (ModuleDataEvent) evt.getOldValue(); + return (null != eventData + && (eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE.getTypeID() + || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT.getTypeID() + || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_CALLLOG.getTypeID() + || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID())); + } + + return false; + } + + /** + * Class to hold the data for setting up the filter panel gui controls. + */ + class FilterPanelData { + + private final Map dataSourceMap; + private final Integer startTime; + private final Integer endTime; + private final List accountTypesInUse; + + FilterPanelData(Map dataSourceMap, List accountTypesInUse, Integer startTime, Integer endTime) { + this.dataSourceMap = dataSourceMap; + this.startTime = startTime; + this.endTime = endTime; + this.accountTypesInUse = accountTypesInUse; + } + + Map getDataSourceMap() { + return dataSourceMap; + } + + Integer getStartTime() { + return startTime; + } + + Integer getEndTime() { + return endTime; + } + + List getAccountTypesInUse() { + return accountTypesInUse; + } + + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.java b/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.java index 5f62c73c67..fbd41b5840 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.java @@ -189,7 +189,7 @@ public final class CVTTopComponent extends TopComponent { * * Re-applying the filters means we will lose the selection... */ - filtersPane.updateAndApplyFilters(true); + filtersPane.initalizeFilters(); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.form b/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.form index baa62440be..8598d04494 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.form +++ b/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.form @@ -18,11 +18,11 @@ + + - - @@ -222,9 +222,14 @@ + + + + + - + @@ -277,7 +282,6 @@ - diff --git a/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.java b/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.java index 70f5de0a5e..90c41e467f 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.java @@ -18,25 +18,24 @@ */ package org.sleuthkit.autopsy.communications; +import org.sleuthkit.autopsy.guiutils.RefreshThrottler; import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.Subscribe; import java.awt.event.ItemListener; import java.beans.PropertyChangeListener; -import java.sql.ResultSet; -import java.sql.SQLException; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; +import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.logging.Level; import java.util.stream.Collectors; import javax.swing.Box; import javax.swing.BoxLayout; @@ -45,11 +44,9 @@ import javax.swing.ImageIcon; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.SwingWorker; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import static org.sleuthkit.autopsy.casemodule.Case.Events.CURRENT_CASE; -import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; @@ -59,7 +56,6 @@ import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.DATA_ import org.sleuthkit.autopsy.ingest.ModuleDataEvent; import org.sleuthkit.datamodel.Account; import org.sleuthkit.datamodel.BlackboardArtifact; -import org.sleuthkit.datamodel.CaseDbAccessManager.CaseDbAccessQueryCallback; import org.sleuthkit.datamodel.CommunicationsFilter; import org.sleuthkit.datamodel.CommunicationsFilter.AccountTypeFilter; import org.sleuthkit.datamodel.CommunicationsFilter.DateRangeFilter; @@ -69,8 +65,6 @@ import org.sleuthkit.datamodel.DataSource; import static org.sleuthkit.datamodel.Relationship.Type.CALL_LOG; import static org.sleuthkit.datamodel.Relationship.Type.CONTACT; import static org.sleuthkit.datamodel.Relationship.Type.MESSAGE; -import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.datamodel.TskCoreException; /** * Panel that holds the Filter control widgets and triggers queries against the @@ -114,6 +108,8 @@ final public class FiltersPanel extends JPanel { */ private final ItemListener validationListener; + private final RefreshThrottler refreshThrottler; + /** * Is the device account type filter enabled or not. It should be enabled * when the Table/Brows mode is active and disabled when the visualization @@ -129,6 +125,7 @@ final public class FiltersPanel extends JPanel { initComponents(); initalizeDeviceAccountType(); + setDateTimeFiltersToDefault(); deviceRequiredLabel.setVisible(false); accountTypeRequiredLabel.setVisible(false); @@ -160,25 +157,27 @@ final public class FiltersPanel extends JPanel { if (eventType.equals(DATA_ADDED.toString())) { // Indicate that a refresh may be needed, unless the data added is Keyword or Hashset hits ModuleDataEvent eventData = (ModuleDataEvent) pce.getOldValue(); - if (null != eventData + if (!needsRefresh + && null != eventData && (eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE.getTypeID() || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT.getTypeID() || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_CALLLOG.getTypeID() || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID())) { - updateFilters(true); needsRefresh = true; validateFilters(); } } }; + refreshThrottler = new RefreshThrottler(new FilterPanelRefresher(false, false)); + this.ingestJobListener = pce -> { String eventType = pce.getPropertyName(); - if (eventType.equals(COMPLETED.toString()) - && updateFilters(true)) { + if (eventType.equals(COMPLETED.toString()) && !needsRefresh) { needsRefresh = true; validateFilters(); + } }; @@ -220,39 +219,24 @@ final public class FiltersPanel extends JPanel { } } - /** - * Update the filter widgets, and apply them. - */ - void updateAndApplyFilters(boolean initialState) { - updateFilters(initialState); - applyFilters(); - initalizeDateTimeFilters(); + void initalizeFilters() { + Runnable runnable = new Runnable() { + @Override + public void run() { + new FilterPanelRefresher(true, true).refresh(); + } + }; + runnable.run(); } private void updateTimeZone() { dateRangeLabel.setText("Date Range (" + Utils.getUserPreferredZoneId().toString() + "):"); } - /** - * Updates the filter widgets to reflect he data sources/types in the case. - */ - private boolean updateFilters(boolean initialState) { - final SleuthkitCase sleuthkitCase; - try { - sleuthkitCase = Case.getCurrentCaseThrows().getSleuthkitCase(); - } catch (NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Unable to perform filter update, update has been cancelled. Case is closed.", ex); - return false; - } - boolean newAccountType = updateAccountTypeFilter(initialState, sleuthkitCase); - boolean newDeviceFilter = updateDeviceFilter(initialState, sleuthkitCase); - // both or either are true, return true; - return newAccountType || newDeviceFilter; - } - @Override public void addNotify() { super.addNotify(); + refreshThrottler.registerForIngestModuleEvents(); IngestManager.getInstance().addIngestModuleEventListener(INGEST_MODULE_EVENTS_OF_INTEREST, ingestListener); IngestManager.getInstance().addIngestJobEventListener(INGEST_JOB_EVENTS_OF_INTEREST, ingestJobListener); Case.addEventTypeSubscriber(EnumSet.of(CURRENT_CASE), evt -> { @@ -270,6 +254,7 @@ final public class FiltersPanel extends JPanel { @Override public void removeNotify() { super.removeNotify(); + refreshThrottler.unregisterEventListener(); IngestManager.getInstance().removeIngestModuleEventListener(ingestListener); IngestManager.getInstance().removeIngestJobEventListener(ingestJobListener); } @@ -283,33 +268,26 @@ final public class FiltersPanel extends JPanel { /** * Populate the Account Types filter widgets. * - * @param selected The initial value for the account type checkbox. - * @param sleuthkitCase The sleuthkit case for containing the account - * information. + * @param accountTypesInUse List of accountTypes currently in use + * @param checkNewOnes * * @return True, if a new accountType was found */ - private boolean updateAccountTypeFilter(boolean selected, SleuthkitCase sleuthkitCase) { + private boolean updateAccountTypeFilter(List accountTypesInUse, boolean checkNewOnes) { boolean newOneFound = false; - try { - List accountTypesInUse = sleuthkitCase.getCommunicationsManager().getAccountTypesInUse(); - for (Account.Type type : accountTypesInUse) { + for (Account.Type type : accountTypesInUse) { + if (!accountTypeMap.containsKey(type) && !type.equals(Account.Type.CREDIT_CARD)) { + CheckBoxIconPanel panel = createAccoutTypeCheckBoxPanel(type, checkNewOnes); + accountTypeMap.put(type, panel.getCheckBox()); + accountTypeListPane.add(panel); - if (!accountTypeMap.containsKey(type) && !type.equals(Account.Type.CREDIT_CARD)) { - CheckBoxIconPanel panel = createAccoutTypeCheckBoxPanel(type, selected); - accountTypeMap.put(type, panel.getCheckBox()); - accountTypeListPane.add(panel); - - newOneFound = true; - } + newOneFound = true; } - - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to update to update Account Types Filter", ex); } + if (newOneFound) { - accountTypeListPane.revalidate(); + accountTypeListPane.validate(); } return newOneFound; @@ -337,38 +315,48 @@ final public class FiltersPanel extends JPanel { /** * Populate the devices filter widgets. * - * @param selected Sets the initial state of device check box. - * @param sleuthkitCase The sleuthkit case for containing the data source - * information. + * @param dataSourceMap + * @param checkNewOnes * * @return true if a new device was found */ - private boolean updateDeviceFilter(boolean selected, SleuthkitCase sleuthkitCase) { + private void updateDeviceFilterPanel(Map dataSourceMap, boolean checkNewOnes) { boolean newOneFound = false; - try { - for (DataSource dataSource : sleuthkitCase.getDataSources()) { - String dsName = sleuthkitCase.getContentById(dataSource.getId()).getName(); - if (devicesMap.containsKey(dataSource.getDeviceId())) { - continue; - } - - final JCheckBox jCheckBox = new JCheckBox(dsName, selected); - jCheckBox.addItemListener(validationListener); - devicesListPane.add(jCheckBox); - devicesMap.put(dataSource.getDeviceId(), jCheckBox); - - newOneFound = true; - + for (Entry entry : dataSourceMap.entrySet()) { + if (devicesMap.containsKey(entry.getValue().getDeviceId())) { + continue; } - } catch (TskCoreException tskCoreException) { - logger.log(Level.SEVERE, "There was a error loading the datasources for the case.", tskCoreException); + + final JCheckBox jCheckBox = new JCheckBox(entry.getKey(), checkNewOnes); + jCheckBox.addItemListener(validationListener); + jCheckBox.setToolTipText(entry.getKey()); + devicesListPane.add(jCheckBox); + devicesMap.put(entry.getValue().getDeviceId(), jCheckBox); + + newOneFound = true; } if (newOneFound) { + devicesListPane.removeAll(); + List checkList = new ArrayList<>(devicesMap.values()); + checkList.sort(new DeviceCheckBoxComparator()); + + for (JCheckBox cb : checkList) { + devicesListPane.add(cb); + } + devicesListPane.revalidate(); } + } - return newOneFound; + private void updateDateTimePicker(Integer start, Integer end) { + if (start != null && start != 0) { + startDatePicker.setDate(LocalDateTime.ofInstant(Instant.ofEpochSecond(start), Utils.getUserPreferredZoneId()).toLocalDate()); + } + + if (end != null && end != 0) { + endDatePicker.setDate(LocalDateTime.ofInstant(Instant.ofEpochSecond(end), Utils.getUserPreferredZoneId()).toLocalDate()); + } } /** @@ -477,9 +465,9 @@ final public class FiltersPanel extends JPanel { setLayout(new java.awt.GridBagLayout()); - scrollPane.setBorder(null); scrollPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); scrollPane.setAutoscrolls(true); + scrollPane.setBorder(null); mainPanel.setLayout(new java.awt.GridBagLayout()); @@ -613,6 +601,7 @@ final public class FiltersPanel extends JPanel { gridBagConstraints.insets = new java.awt.Insets(15, 0, 0, 25); mainPanel.add(dateRangePane, gridBagConstraints); + devicesPane.setPreferredSize(new java.awt.Dimension(300, 300)); devicesPane.setLayout(new java.awt.GridBagLayout()); unCheckAllDevicesButton.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.unCheckAllDevicesButton.text")); // NOI18N @@ -652,7 +641,6 @@ final public class FiltersPanel extends JPanel { gridBagConstraints.insets = new java.awt.Insets(9, 0, 0, 0); devicesPane.add(checkAllDevicesButton, gridBagConstraints); - devicesScrollPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); devicesScrollPane.setMaximumSize(new java.awt.Dimension(32767, 30)); devicesScrollPane.setMinimumSize(new java.awt.Dimension(27, 30)); devicesScrollPane.setPreferredSize(new java.awt.Dimension(3, 30)); @@ -686,10 +674,10 @@ final public class FiltersPanel extends JPanel { gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 2; - gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; - gridBagConstraints.ipady = 100; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; gridBagConstraints.insets = new java.awt.Insets(15, 0, 0, 25); mainPanel.add(devicesPane, gridBagConstraints); @@ -836,10 +824,11 @@ final public class FiltersPanel extends JPanel { /** * Post an event with the new filters. */ - private void applyFilters() { - CVTEvents.getCVTEventBus().post(new CVTEvents.FilterChangeEvent(getFilter(), getStartControlState(), getEndControlState())); + void applyFilters() { needsRefresh = false; validateFilters(); + CVTEvents.getCVTEventBus().post(new CVTEvents.FilterChangeEvent(getFilter(), getStartControlState(), getEndControlState())); + } /** @@ -958,31 +947,6 @@ final public class FiltersPanel extends JPanel { map.values().forEach(box -> box.setSelected(selected)); } - /** - * initalize the DateTimePickers by grabbing the earliest and latest time - * from the autopsy db. - */ - private void initalizeDateTimeFilters() { - Case currentCase = null; - try { - currentCase = Case.getCurrentCaseThrows(); - } catch (NoCurrentCaseException ex) { - logger.log(Level.INFO, "Tried to intialize communication filters date range filters without an open case, using default values"); - } - - if (currentCase == null) { - setDateTimeFiltersToDefault(); - openCase = null; - return; - } - - if (!currentCase.equals(openCase)) { - setDateTimeFiltersToDefault(); - openCase = currentCase; - (new DatePickerWorker()).execute(); - } - } - private void setDateTimeFiltersToDefault() { startDatePicker.setDate(LocalDate.now().minusWeeks(3)); endDatePicker.setDate(LocalDate.now()); @@ -1159,69 +1123,51 @@ final public class FiltersPanel extends JPanel { } /** - * A simple class that implements CaseDbAccessQueryCallback. Can be used as - * an anonymous innerclass with the CaseDbAccessManager select function. + * Extends the CVTFilterRefresher abstract class to add the calls to update + * the ui controls with the data found. Note that updateFilterPanel is run + * in the EDT. */ - class FilterPanelQueryCallback implements CaseDbAccessQueryCallback { + final class FilterPanelRefresher extends CVTFilterRefresher { + + private final boolean selectNewOption; + private final boolean refreshAfterUpdate; + + FilterPanelRefresher(boolean selectNewOptions, boolean refreshAfterUpdate) { + this.selectNewOption = selectNewOptions; + this.refreshAfterUpdate = refreshAfterUpdate; + } @Override - public void process(ResultSet rs) { - // Subclasses can implement their own process function. + void updateFilterPanel(CVTFilterRefresher.FilterPanelData data) { + updateDateTimePicker(data.getStartTime(), data.getEndTime()); + updateDeviceFilterPanel(data.getDataSourceMap(), selectNewOption); + updateAccountTypeFilter(data.getAccountTypesInUse(), selectNewOption); + + FiltersPanel.this.repaint(); + + if (refreshAfterUpdate) { + applyFilters(); + } + + if (!isEnabled()) { + setEnabled(true); + } + + validateFilters(); + + repaint(); } } - final class DatePickerWorker extends SwingWorker, Void> { + /** + * Sorts a list of JCheckBoxes in alphabetical order of the text field + * value. + */ + class DeviceCheckBoxComparator implements Comparator { @Override - protected Map doInBackground() throws Exception { - if (openCase == null) { - return null; - } - - Map resultMap = new HashMap<>(); - String queryString = "max(date_time) as end, min(date_time) as start from account_relationships"; // NON-NLS - - openCase.getSleuthkitCase().getCaseDbAccessManager().select(queryString, new FilterPanelQueryCallback() { - @Override - public void process(ResultSet rs) { - try { - if (rs.next()) { - int startDate = rs.getInt("start"); // NON-NLS - int endDate = rs.getInt("end"); // NON-NLS - - resultMap.put("start", startDate); // NON-NLS - resultMap.put("end", endDate); // NON-NLS - } - } catch (SQLException ex) { - // Not the end of the world if this fails. - logger.log(Level.WARNING, String.format("SQL Exception thrown from Query: %s", queryString), ex); - } - } - }); - - return resultMap; - } - - @Override - protected void done() { - try { - Map resultMap = get(); - if (resultMap != null) { - Integer start = resultMap.get("start"); - Integer end = resultMap.get("end"); - - if (start != null && start != 0) { - startDatePicker.setDate(LocalDateTime.ofInstant(Instant.ofEpochSecond(start), Utils.getUserPreferredZoneId()).toLocalDate()); - } - - if (end != null && end != 0) { - endDatePicker.setDate(LocalDateTime.ofInstant(Instant.ofEpochSecond(end), Utils.getUserPreferredZoneId()).toLocalDate()); - } - } - } catch (InterruptedException | ExecutionException ex) { - logger.log(Level.WARNING, "Exception occured after date time sql query", ex); - } + public int compare(JCheckBox e1, JCheckBox e2) { + return e1.getText().toLowerCase().compareTo(e2.getText().toLowerCase()); } } - } diff --git a/Core/src/org/sleuthkit/autopsy/communications/Utils.java b/Core/src/org/sleuthkit/autopsy/communications/Utils.java index 4a3e03e1f2..3309bab92e 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/Utils.java +++ b/Core/src/org/sleuthkit/autopsy/communications/Utils.java @@ -18,9 +18,12 @@ */ package org.sleuthkit.autopsy.communications; +import java.awt.Component; import java.time.ZoneId; import java.time.ZoneOffset; import java.util.TimeZone; +import javax.swing.table.TableCellRenderer; +import org.netbeans.swing.outline.Outline; import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.datamodel.accounts.Accounts; import org.sleuthkit.datamodel.Account; @@ -47,5 +50,29 @@ public final class Utils { static public final String getIconFilePath(Account.Type type) { return Accounts.getIconFilePath(type); } + + static public void setColumnWidths(Outline outline) { + int margin = 4; + int padding = 8; + + final int rows = Math.min(100, outline.getRowCount()); + + for (int column = 0; column < outline.getColumnCount(); column++) { + int columnWidthLimit = 500; + int columnWidth = 0; + + // find the maximum width needed to fit the values for the first 100 rows, at most + for (int row = 0; row < rows; row++) { + TableCellRenderer renderer = outline.getCellRenderer(row, column); + Component comp = outline.prepareRenderer(renderer, row, column); + columnWidth = Math.max(comp.getPreferredSize().width, columnWidth); + } + + columnWidth += 2 * margin + padding; // add margin and regular padding + columnWidth = Math.min(columnWidth, columnWidthLimit); + + outline.getColumnModel().getColumn(column).setPreferredWidth(columnWidth); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/AccountSummary.java b/Core/src/org/sleuthkit/autopsy/communications/relationships/AccountSummary.java index ff6607a84e..8ac88abc51 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/AccountSummary.java +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/AccountSummary.java @@ -33,6 +33,7 @@ import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.blackboardutils.attributes.MessageAttachments.FileAttachment; import org.sleuthkit.datamodel.blackboardutils.attributes.MessageAttachments; import org.sleuthkit.datamodel.CommunicationsUtils; +import org.sleuthkit.datamodel.InvalidAccountIDException; import org.sleuthkit.datamodel.blackboardutils.attributes.BlackboardJsonAttrUtil; /** @@ -113,7 +114,7 @@ class AccountSummary { isReference = true; break; } - } catch (TskCoreException ex) { + } catch (InvalidAccountIDException ex) { logger.log(Level.WARNING, String.format("Exception thrown " + "in trying to normalize attribute value: %s", attributeValue), ex); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties b/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties index b14d8a2688..6ecb170c10 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties @@ -9,7 +9,6 @@ SummaryViewer.callLogsLabel.text=Call Logs: ThreadRootMessagePanel.showAllCheckBox.text=Show All Messages ThreadPane.backButton.text=<--- SummaryViewer.caseReferencesPanel.border.title=Other Occurrences -SummaryViewer.fileReferencesPanel.border.title=File References in Current Case MessageViewer.threadsLabel.text=Select a Thread to View MessageViewer.threadNameLabel.text= MessageViewer.showingMessagesLabel.text=Showing Messages for Thread: @@ -27,3 +26,5 @@ SummaryViewer.referencesLabel.text=Communication References: SummaryViewer.referencesDataLabel.text= SummaryViewer.contactsLabel.text=Book Entries: SummaryViewer.accountCountry.text= +SummaryViewer.fileRefPane.border.title=File References in Current Case +SummaryViewer.selectAccountFileRefLabel.text= SummaryViewer_FileRefNameColumn_Title=Path SummaryViewer_TabTitle=Summary ThreadRootMessagePanel.showAllCheckBox.text=Show All Messages ThreadPane.backButton.text=<--- SummaryViewer.caseReferencesPanel.border.title=Other Occurrences -SummaryViewer.fileReferencesPanel.border.title=File References in Current Case MessageViewer.threadsLabel.text=Select a Thread to View MessageViewer.threadNameLabel.text= MessageViewer.showingMessagesLabel.text=Showing Messages for Thread: @@ -73,3 +73,5 @@ SummaryViewer.referencesLabel.text=Communication References: SummaryViewer.referencesDataLabel.text= SummaryViewer.contactsLabel.text=Book Entries: SummaryViewer.accountCountry.text= +SummaryViewer.fileRefPane.border.title=File References in Current Case +SummaryViewer.selectAccountFileRefLabel.text= -l ` to update properties files based on the newly generated csv file. The csv file should be formatted such that the columns are bundle relative path, property files key, translated value and commit id for the latest commit id for which these changes represent. The commit id only needs to be in the header row. The output path should be specified as a relative path with the dot slash notation (i.e. `./outputpath.csv`) or an absolute path. + +## Localization Generation for the First Time +First-time updates should follow a similar procedure except that instead of calling `diffscript.py`, call `python3 allbundlesscript ` to generate a csv file with relative paths of bundle files, property file keys, property file values. The output path should be specified as a relative path with the dot slash notation (i.e. `./inputpath.csv`) or an absolute path. + +##Unit Tests +Unit tests can be run from this directory using `python3 -m unittest`. \ No newline at end of file diff --git a/release_scripts/localization_scripts/__init__.py b/release_scripts/localization_scripts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/release_scripts/localization_scripts/allbundlesscript.py b/release_scripts/localization_scripts/allbundlesscript.py new file mode 100644 index 0000000000..139dec15a5 --- /dev/null +++ b/release_scripts/localization_scripts/allbundlesscript.py @@ -0,0 +1,73 @@ +"""This script finds all '.properties-MERGED' files and writes relative path, key, and value to a CSV file. +This script requires the python libraries: gitpython and jproperties. As a consequence, it also requires +git >= 1.7.0 and python >= 3.4. This script relies on fetching 'HEAD' from current branch. So make sure +repo is on correct branch (i.e. develop). +""" + +import sys + +from envutil import get_proj_dir +from fileutil import get_filename_addition, OMITTED_ADDITION +from gitutil import get_property_file_entries, get_commit_id, get_git_root +from csvutil import records_to_csv +from typing import Union +import re +import argparse + + +def write_items_to_csv(repo_path: str, output_path: str, show_commit: bool, value_regex: Union[str, None] = None): + """Determines the contents of '.properties-MERGED' files and writes to a csv file. + + Args: + repo_path (str): The local path to the git repo. + output_path (str): The output path for the csv file. + show_commit (bool): Whether or not to include the commit id in the header + value_regex (Union[str, None]): If non-none, only key value pairs where the value is a regex match with this + value will be included. + """ + + row_header = ['Relative path', 'Key', 'Value'] + if show_commit: + row_header.append(get_commit_id(repo_path, 'HEAD')) + + rows = [] + omitted = [] + + for entry in get_property_file_entries(repo_path): + new_entry = [entry.rel_path, entry.key, entry.value] + if value_regex is None or re.match(value_regex, entry.value): + rows.append(new_entry) + else: + omitted.append(new_entry) + + records_to_csv(output_path, [row_header] + rows) + + if len(omitted) > 0: + records_to_csv(get_filename_addition(output_path, OMITTED_ADDITION), [row_header] + omitted) + + +def main(): + # noinspection PyTypeChecker + parser = argparse.ArgumentParser(description='Gathers all key-value pairs within .properties-MERGED files into ' + 'one csv file.', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument(dest='output_path', type=str, help='The path to the output csv file. The output path should be' + ' specified as a relative path with the dot slash notation ' + '(i.e. \'./outputpath.csv\') or an absolute path.') + parser.add_argument('-r', '--repo', dest='repo_path', type=str, required=False, + help='The path to the repo. If not specified, path of script is used.') + parser.add_argument('-nc', '--no_commit', dest='no_commit', action='store_true', default=False, + required=False, help="Suppresses adding commits to the generated csv header.") + + args = parser.parse_args() + repo_path = args.repo_path if args.repo_path is not None else get_git_root(get_proj_dir()) + output_path = args.output_path + show_commit = not args.no_commit + + write_items_to_csv(repo_path, output_path, show_commit) + + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/release_scripts/localization_scripts/csvutil.py b/release_scripts/localization_scripts/csvutil.py new file mode 100644 index 0000000000..daa66f5396 --- /dev/null +++ b/release_scripts/localization_scripts/csvutil.py @@ -0,0 +1,51 @@ +"""Provides tools for parsing and writing to a csv file. +""" +from typing import List, Iterable, Tuple +import csv +import os + + +def records_to_csv(output_path: str, rows: Iterable[List[str]]): + """Writes rows to a csv file at the specified path. + + Args: + output_path (str): The path where the csv file will be written. + rows (List[List[str]]): The rows to be written. Each row of a + list of strings will be written according + to their index (i.e. column 3 will be index 2). + """ + + parent_dir, file = os.path.split(output_path) + if not os.path.exists(parent_dir): + os.makedirs(parent_dir) + + with open(output_path, 'w', encoding="utf-8-sig", newline='') as csvfile: + writer = csv.writer(csvfile) + for row in rows: + writer.writerow(row) + + +def csv_to_records(input_path: str, header_row: bool) -> Tuple[List[List[str]], List[str]]: + """Writes rows to a csv file at the specified path. + + Args: + input_path (str): The path where the csv file will be written. + header_row (bool): Whether or not there is a header row to be skipped. + """ + + with open(input_path, encoding='utf-8-sig') as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + + header = None + results = [] + try: + for row in csv_reader: + if header_row: + header = row + header_row = False + else: + results.append(row) + except Exception as e: + raise Exception("There was an error parsing csv {path}".format(path=input_path), e) + + return results, header diff --git a/release_scripts/localization_scripts/diffscript.py b/release_scripts/localization_scripts/diffscript.py new file mode 100644 index 0000000000..2713fef518 --- /dev/null +++ b/release_scripts/localization_scripts/diffscript.py @@ -0,0 +1,97 @@ +"""This script determines the updated, added, and deleted properties from the '.properties-MERGED' files +and generates a csv file containing the items changed. This script requires the python libraries: +gitpython and jproperties. As a consequence, it also requires git >= 1.7.0 and python >= 3.4. +""" +import re +import sys +from envutil import get_proj_dir +from fileutil import get_filename_addition, OMITTED_ADDITION +from gitutil import get_property_files_diff, get_commit_id, get_git_root +from itemchange import ItemChange, ChangeType +from csvutil import records_to_csv +import argparse +from typing import Union +from langpropsutil import get_commit_for_language, LANG_FILENAME + + +def write_diff_to_csv(repo_path: str, output_path: str, commit_1_id: str, commit_2_id: str, show_commits: bool, + value_regex: Union[str, None] = None): + """Determines the changes made in '.properties-MERGED' files from one commit to another commit. + + Args: + repo_path (str): The local path to the git repo. + output_path (str): The output path for the csv file. + commit_1_id (str): The initial commit for the diff. + commit_2_id (str): The latest commit for the diff. + show_commits (bool): Show commits in the header row. + value_regex (Union[str, None]): If non-none, only key value pairs where the value is a regex match with this + value will be included. + """ + + row_header = ItemChange.get_headers() + if show_commits: + row_header += [get_commit_id(repo_path, commit_1_id), get_commit_id(repo_path, commit_2_id)] + + rows = [] + omitted = [] + + for entry in get_property_files_diff(repo_path, commit_1_id, commit_2_id): + new_entry = entry.get_row() + if value_regex is not None and (entry.type == ChangeType.DELETION or not re.match(value_regex, entry.cur_val)): + omitted.append(new_entry) + else: + rows.append(new_entry) + + records_to_csv(output_path, [row_header] + rows) + + if len(omitted) > 0: + records_to_csv(get_filename_addition(output_path, OMITTED_ADDITION), [row_header] + omitted) + + +def main(): + # noinspection PyTypeChecker + parser = argparse.ArgumentParser(description="Determines the updated, added, and deleted properties from the " + "'.properties-MERGED' files and generates a csv file containing " + "the items changed.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument(dest='output_path', type=str, help='The path to the output csv file. The output path should ' + 'be specified as a relative path with the dot slash notation' + ' (i.e. \'./outputpath.csv\') or an absolute path.') + + parser.add_argument('-r', '--repo', dest='repo_path', type=str, required=False, + help='The path to the repo. If not specified, path of script is used.') + parser.add_argument('-fc', '--first-commit', dest='commit_1_id', type=str, required=False, + help='The commit for previous release. This flag or the language flag need to be specified' + ' in order to determine a start point for the difference.') + parser.add_argument('-lc', '--latest-commit', dest='commit_2_id', type=str, default='HEAD', required=False, + help='The commit for current release.') + parser.add_argument('-nc', '--no-commits', dest='no_commits', action='store_true', default=False, + required=False, help="Suppresses adding commits to the generated csv header.") + parser.add_argument('-l', '--language', dest='language', type=str, default=None, required=False, + help='Specify the language in order to determine the first commit to use (i.e. \'ja\' for ' + 'Japanese. This flag overrides the first-commit flag.') + + args = parser.parse_args() + repo_path = args.repo_path if args.repo_path is not None else get_git_root(get_proj_dir()) + output_path = args.output_path + commit_1_id = args.commit_1_id + lang = args.language + if lang is not None: + commit_1_id = get_commit_for_language(lang) + + if commit_1_id is None: + print('Either the first commit or language flag need to be specified. If specified, the language file, ' + + LANG_FILENAME + ', may not have the latest commit for the language.', file=sys.stderr) + parser.print_help(sys.stderr) + sys.exit(1) + + commit_2_id = args.commit_2_id + show_commits = not args.no_commits + + write_diff_to_csv(repo_path, output_path, commit_1_id, commit_2_id, show_commits) + + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/release_scripts/localization_scripts/envutil.py b/release_scripts/localization_scripts/envutil.py new file mode 100644 index 0000000000..cec2a00eda --- /dev/null +++ b/release_scripts/localization_scripts/envutil.py @@ -0,0 +1,17 @@ +"""Functions relating to the project environment. +""" + +import pathlib +from typing import Union + + +def get_proj_dir(path: Union[pathlib.PurePath, str] = __file__) -> str: + """ + Gets parent directory of this file (and subsequently, the project). + Args: + path: Can be overridden to provide a different file. This will return the parent of that file in that instance. + + Returns: + The project folder or the parent folder of the file provided. + """ + return str(pathlib.Path(path).parent.absolute()) diff --git a/release_scripts/localization_scripts/fileutil.py b/release_scripts/localization_scripts/fileutil.py new file mode 100644 index 0000000000..5139812db2 --- /dev/null +++ b/release_scripts/localization_scripts/fileutil.py @@ -0,0 +1,63 @@ +import os +from typing import Union, Tuple +from pathlib import Path + + +def get_path_pieces(orig_path: str) -> Tuple[str, Union[str, None], Union[str, None]]: + """Retrieves path pieces. This is a naive approach as it determines if a file is present based on the + presence of an extension. + Args: + orig_path: The original path to deconstruct. + + Returns: A tuple of directory, filename and extension. If no extension is present, filename and extension are None. + + """ + + potential_parent_dir, orig_file = os.path.split(str(Path(orig_path))) + filename, file_extension = os.path.splitext(orig_file) + if file_extension.startswith('.'): + file_extension = file_extension[1:] + + if file_extension is None or len(file_extension) < 1: + return str(Path(orig_path)), None, None + else: + return potential_parent_dir, filename, file_extension + + +def get_new_path(orig_path: str, new_filename: str) -> str: + """Obtains a new path. This tries to determine if the provided path is a directory or filename (has an + extension containing '.') then constructs the new path with the old parent directory and the new filename. + + Args: + orig_path (str): The original path. + new_filename (str): The new filename to use. + + Returns: + str: The new path. + """ + + parent_dir, filename, ext = get_path_pieces(orig_path) + return str(Path(parent_dir) / Path(new_filename)) + + +# For use with creating csv filenames for entries that have been omitted. +OMITTED_ADDITION = '-omitted' + + +def get_filename_addition(orig_path: str, filename_addition: str) -> str: + """Gets filename with addition. So if item is '/path/name.ext' and the filename_addition is '-add', the new result + would be '/path/name-add.ext'. + + Args: + orig_path (str): The original path. + filename_addition (str): The new addition. + + Returns: The altered path. + + """ + parent_dir, filename, extension = get_path_pieces(orig_path) + if filename is None: + return str(Path(orig_path + filename_addition)) + else: + ext = '' if extension is None else extension + return str(Path(parent_dir) / Path('{0}{1}.{2}'.format(filename, filename_addition, ext))) diff --git a/release_scripts/localization_scripts/gitutil.py b/release_scripts/localization_scripts/gitutil.py new file mode 100644 index 0000000000..43c20e2ce0 --- /dev/null +++ b/release_scripts/localization_scripts/gitutil.py @@ -0,0 +1,168 @@ +"""Functions relating to using git and GitPython with an existing repo. +""" + +from git import Repo, Diff, Blob +from typing import List, Union, Iterator, Tuple, Any +from itemchange import ItemChange, get_changed +from pathlib import Path +from propentry import PropEntry +from propsutil import DEFAULT_PROPS_EXTENSION, get_entry_dict + + +def get_text(blob: Blob) -> str: + return blob.data_stream.read().decode('utf-8') + + +def get_git_root(child_path: str) -> str: + """ + Taken from https://stackoverflow.com/questions/22081209/find-the-root-of-the-git-repository-where-the-file-lives, + this obtains the root path of the git repo in which this file exists. + Args: + child_path: The path of a child within the repo. + + Returns: The repo root path. + + """ + git_repo = Repo(child_path, search_parent_directories=True) + git_root = git_repo.git.rev_parse("--show-toplevel") + return git_root + + +def get_changed_from_diff(rel_path: str, diff: Diff) -> List[ItemChange]: + """Determines changes from a git python diff. + + Args: + rel_path (str): The relative path for the properties file. + diff (Diff): The git python diff. + + Returns: + List[ItemChange]: The changes in properties. + """ + + # an item was added + if diff.change_type == 'A': + changes = get_changed(rel_path, '', get_text(diff.b_blob)) + # an item was deleted + elif diff.change_type == 'D': + changes = get_changed(rel_path, get_text(diff.a_blob), '') + # an item was modified + elif diff.change_type == 'M': + changes = get_changed(rel_path, get_text( + diff.a_blob), get_text(diff.b_blob)) + else: + changes = [] + + return changes + + +def get_rel_path(diff: Diff) -> Union[str, None]: + """Determines the relative path based on the git python. + + Args: + diff: The git python diff. + + Returns: + str: The determined relative path. + """ + if diff.b_path is not None: + return diff.b_path + elif diff.a_path is not None: + return diff.a_path + else: + return None + + +def get_diff(repo_path: str, commit_1_id: str, commit_2_id: str) -> Any: + """Determines the diff between two commits. + + Args: + repo_path (str): The local path to the git repo. + commit_1_id (str): The initial commit for the diff. + commit_2_id (str): The latest commit for the diff. + + Returns: + The determined diff. + """ + repo = Repo(repo_path, search_parent_directories=True) + commit_1 = repo.commit(commit_1_id) + commit_2 = repo.commit(commit_2_id) + return commit_1.diff(commit_2) + + +def get_commit_id(repo_path: str, commit_id: str) -> str: + """Determines the hash for head commit. This does things like fetch the id of head if 'HEAD' is provided. + + Args: + repo_path: The path to the repo. + commit_id: The id for the commit. + + Returns: + The hash for the commit in the repo. + """ + repo = Repo(repo_path, search_parent_directories=True) + commit = repo.commit(commit_id.strip()) + return str(commit.hexsha) + + +def get_property_files_diff(repo_path: str, commit_1_id: str, commit_2_id: str, + property_file_extension: str = DEFAULT_PROPS_EXTENSION) -> Iterator[ItemChange]: + """Determines the item changes within property files as a diff between two commits. + + Args: + repo_path (str): The repo path. + commit_1_id (str): The first git commit. + commit_2_id (str): The second git commit. + property_file_extension (str): The extension for properties files to gather. + + Returns: + All found item changes in values of keys between the property files. + """ + + diffs = get_diff(repo_path, commit_1_id.strip(), commit_2_id.strip()) + for diff in diffs: + rel_path = get_rel_path(diff) + if rel_path is None or not rel_path.endswith('.' + property_file_extension): + continue + + yield from get_changed_from_diff(rel_path, diff) + + +def list_paths(root_tree, path: Path = Path('.')) -> Iterator[Tuple[str, Blob]]: + """ + Given the root path to serve as a prefix, walks the tree of a git commit returning all files and blobs. + Repurposed from: https://www.enricozini.org/blog/2019/debian/gitpython-list-all-files-in-a-git-commit/ + + Args: + root_tree: The tree of the commit to walk. + path: The path to use as a prefix. + + Returns: A tuple iterator where each tuple consists of the path as a string and a blob of the file. + + """ + for blob in root_tree.blobs: + ret_item = (str(path / blob.name), blob) + yield ret_item + for tree in root_tree.trees: + yield from list_paths(tree, path / tree.name) + + +def get_property_file_entries(repo_path: str, at_commit: str = 'HEAD', + property_file_extension: str = DEFAULT_PROPS_EXTENSION) -> Iterator[PropEntry]: + """ + Retrieves all property files entries returning as an iterator of PropEntry objects. + + Args: + repo_path: The path to the git repo. + at_commit: The commit to use. + property_file_extension: The extension to use for scanning for property files. + + Returns: An iterator of PropEntry objects. + + """ + repo = Repo(repo_path, search_parent_directories=True) + commit = repo.commit(at_commit.strip()) + for item in list_paths(commit.tree): + path, blob = item + if path.endswith(property_file_extension): + for key, val in get_entry_dict(get_text(blob)).items(): + yield PropEntry(path, key, val) diff --git a/release_scripts/localization_scripts/itemchange.py b/release_scripts/localization_scripts/itemchange.py new file mode 100644 index 0000000000..27448cb529 --- /dev/null +++ b/release_scripts/localization_scripts/itemchange.py @@ -0,0 +1,103 @@ +from typing import Iterator, List, Union +from propsutil import get_entry_dict +from enum import Enum + + +class ChangeType(Enum): + """Describes the nature of a change in the properties file.""" + ADDITION = 'ADDITION' + DELETION = 'DELETION' + CHANGE = 'CHANGE' + + def __str__(self): + return str(self.value) + + +class ItemChange: + rel_path: str + key: str + prev_val: Union[str, None] + cur_val: Union[str, None] + type: ChangeType + + def __init__(self, rel_path: str, key: str, prev_val: str, cur_val: str): + """Describes the change that occurred for a particular key of a properties file. + + Args: + rel_path (str): The relative path of the properties file. + key (str): The key in the properties file. + prev_val (str): The previous value for the key. + cur_val (str): The current value for the key. + """ + self.rel_path = rel_path + self.key = key + self.prev_val = prev_val + self.cur_val = cur_val + if cur_val is not None and prev_val is None: + self.type = ChangeType.ADDITION + elif cur_val is None and prev_val is not None: + self.type = ChangeType.DELETION + else: + self.type = ChangeType.CHANGE + + @staticmethod + def get_headers() -> List[str]: + """Returns the csv headers to insert when serializing a list of ItemChange objects to csv. + + Returns: + List[str]: The column headers + """ + return ['Relative Path', 'Key', 'Change Type', 'Previous Value', 'Current Value'] + + def get_row(self) -> List[str]: + """Returns the list of values to be entered as a row in csv serialization. + + Returns: + List[str]: The list of values to be entered as a row in csv serialization. + """ + return [ + self.rel_path, + self.key, + self.type, + self.prev_val, + self.cur_val] + + +def get_item_change(rel_path: str, key: str, prev_val: str, cur_val: str) -> Union[ItemChange, None]: + """Returns an ItemChange object if the previous value is not equal to the current value. + + Args: + rel_path (str): The relative path for the properties file. + key (str): The key within the properties file for this potential change. + prev_val (str): The previous value. + cur_val (str): The current value. + + Returns: + ItemChange: The ItemChange object or None if values are the same. + """ + if prev_val == cur_val: + return None + else: + return ItemChange(rel_path, key, prev_val, cur_val) + + +def get_changed(rel_path: str, a_str: str, b_str: str) -> Iterator[ItemChange]: + """Given the relative path of the properties file that has been provided, + determines the property items that have changed between the two property + file strings. + + Args: + rel_path (str): The relative path for the properties file. + a_str (str): The string representing the original state of the file. + b_str (str): The string representing the current state of the file. + + Returns: + List[ItemChange]: The changes determined. + """ + print('Retrieving changes for {0}...'.format(rel_path)) + a_dict = get_entry_dict(a_str) + b_dict = get_entry_dict(b_str) + all_keys = set().union(a_dict.keys(), b_dict.keys()) + mapped = map(lambda key: get_item_change( + rel_path, key, a_dict.get(key), b_dict.get(key)), all_keys) + return filter(lambda entry: entry is not None, mapped) diff --git a/release_scripts/localization_scripts/langpropsutil.py b/release_scripts/localization_scripts/langpropsutil.py new file mode 100644 index 0000000000..841574c9e9 --- /dev/null +++ b/release_scripts/localization_scripts/langpropsutil.py @@ -0,0 +1,34 @@ +"""Functions handling retrieving and storing when a language was last updated. +""" +from typing import Union +from envutil import get_proj_dir +from propsutil import get_entry_dict_from_path, update_entry_dict +from os import path + + +LANG_FILENAME = 'lastupdated.properties' + + +def _get_last_update_key(language: str) -> str: + return "bundles.{lang}.lastupdated".format(lang=language) + + +def _get_props_path(): + return path.join(get_proj_dir(), LANG_FILENAME) + + +def get_commit_for_language(language: str) -> Union[str, None]: + lang_dict = get_entry_dict_from_path(_get_props_path()) + if lang_dict is None: + return None + + key = _get_last_update_key(language) + if key not in lang_dict: + return None + + return lang_dict[key] + + +def set_commit_for_language(language: str, latest_commit: str): + key = _get_last_update_key(language) + update_entry_dict({key: latest_commit}, _get_props_path()) diff --git a/release_scripts/localization_scripts/lastupdated.properties b/release_scripts/localization_scripts/lastupdated.properties new file mode 100644 index 0000000000..db7e961472 --- /dev/null +++ b/release_scripts/localization_scripts/lastupdated.properties @@ -0,0 +1,2 @@ +# in format of bundles..lastupdated= +bundles.ja.lastupdated=d9a37c48f4bd0dff014eead73a0eb730c875ed9f \ No newline at end of file diff --git a/release_scripts/localization_scripts/propentry.py b/release_scripts/localization_scripts/propentry.py new file mode 100644 index 0000000000..99c00f749e --- /dev/null +++ b/release_scripts/localization_scripts/propentry.py @@ -0,0 +1,19 @@ +class PropEntry: + rel_path: str + key: str + value: str + should_delete: bool + + def __init__(self, rel_path: str, key: str, value: str, should_delete: bool = False): + """Defines a property file entry to be updated in a property file. + + Args: + rel_path (str): The relative path for the property file. + key (str): The key for the entry. + value (str): The value for the entry. + should_delete (bool, optional): Whether or not the key should simply be deleted. Defaults to False. + """ + self.rel_path = rel_path + self.key = key + self.value = value + self.should_delete = should_delete diff --git a/release_scripts/localization_scripts/propsutil.py b/release_scripts/localization_scripts/propsutil.py new file mode 100644 index 0000000000..3de52a7966 --- /dev/null +++ b/release_scripts/localization_scripts/propsutil.py @@ -0,0 +1,97 @@ +"""Provides tools for reading from and writing to java properties files. +""" +from typing import Dict, Union, IO +from jproperties import Properties +import os + +# The default extension for property files in autopsy repo + +DEFAULT_PROPS_EXTENSION = 'properties-MERGED' + + +def get_lang_bundle_name(language: str) -> str: + """ + Returns the bundle name for the specific language identifier provided. + Args: + language: The language identifier (i.e. 'ja' for Japanese) + + Returns: + The bundle name + """ + return 'Bundle_{lang}.properties'.format(lang=language) + + +def get_entry_dict(file_contents: Union[str, IO]) -> Dict[str, str]: + """Retrieves a dictionary mapping the properties represented in the string. + + Args: + file_contents: The string of the properties file or the file handle. + + Returns: + Dict[str,str]: The mapping of keys to values in that properties file. + """ + + props = Properties() + try: + props.load(file_contents) + except Exception as e: + raise Exception("There was an error loading properties file {file}".format(file=file_contents), e) + return props.properties + + +def get_entry_dict_from_path(props_path: str) -> Union[Dict[str, str], None]: + """ + Retrieves a dictionary mapping the properties represented in the string or None if no properties file can be found + at that path. + Args: + props_path: The path to the properties file. + + Returns: The entry dictionary for that properties file. + + """ + if os.path.isfile(props_path): + with open(props_path, "rb") as f: + return get_entry_dict(f) + else: + return None + + +def set_entry_dict(contents: Dict[str, str], file_path: str): + """Sets the property file to the key-value pairs of the contents dictionary. + + Args: + contents (Dict[str, str]): The dictionary whose contents will be the key value pairs of the properties file. + file_path (str): The path to the properties file to create. + """ + + props = Properties() + for key, val in contents.items(): + props[key] = val + + parent_dir, file = os.path.split(file_path) + if not os.path.exists(parent_dir): + os.makedirs(parent_dir) + + with open(file_path, "wb") as f: + props.store(f) + + +def update_entry_dict(contents: Dict[str, str], file_path: str): + """Updates the properties file at the given location with the key-value properties of contents. + Creates a new properties file at given path if none exists. + + Args: + contents (Dict[str, str]): The dictionary whose contents will be the key value pairs of the properties file. + file_path (str): The path to the properties file to create. + """ + contents_to_edit = contents.copy() + + cur_dict = get_entry_dict_from_path(file_path) + if cur_dict is None: + cur_dict = {} + for cur_key, cur_val in cur_dict.items(): + # only update contents if contents does not already have key + if cur_key not in contents_to_edit: + contents_to_edit[cur_key] = cur_val + + set_entry_dict(contents_to_edit, file_path) diff --git a/release_scripts/localization_scripts/test/__init__.py b/release_scripts/localization_scripts/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/release_scripts/localization_scripts/test/artifacts/.gitignore b/release_scripts/localization_scripts/test/artifacts/.gitignore new file mode 100644 index 0000000000..6caf68aff4 --- /dev/null +++ b/release_scripts/localization_scripts/test/artifacts/.gitignore @@ -0,0 +1 @@ +output \ No newline at end of file diff --git a/release_scripts/localization_scripts/test/test_csvutil.py b/release_scripts/localization_scripts/test/test_csvutil.py new file mode 100644 index 0000000000..a5ffd0cb71 --- /dev/null +++ b/release_scripts/localization_scripts/test/test_csvutil.py @@ -0,0 +1,41 @@ +import codecs +import os +import unittest +from typing import TypeVar, List + +from csvutil import records_to_csv, csv_to_records +from test.unittestutil import get_output_path + + +class CsvUtilTest(unittest.TestCase): + T = TypeVar('T') + + def assert_equal_arr(self, a: List[T], b: List[T]): + self.assertEqual(len(a), len(b), 'arrays are not equal length') + for i in range(0, len(a)): + if isinstance(a[i], list) and isinstance(b[i], list): + self.assert_equal_arr(a[i], b[i]) + else: + self.assertEqual(a[i], b[i], "Items: {0} and {1} at index {2} are not equal.".format(a[i], b[i], i)) + + def test_read_write(self): + data = [['header1', 'header2', 'header3', 'additional header'], + ['data1', 'data2', 'data3'], + ['', 'data2-1', 'data2-2']] + + os.makedirs(get_output_path(), exist_ok=True) + test_path = get_output_path('test.csv') + records_to_csv(test_path, data) + + byte_inf = min(32, os.path.getsize(test_path)) + with open(test_path, 'rb') as bom_test_file: + raw = bom_test_file.read(byte_inf) + if not raw.startswith(codecs.BOM_UTF8): + self.fail("written csv does not have appropriate BOM") + + read_records_no_header, no_header = csv_to_records(test_path, header_row=False) + self.assert_equal_arr(read_records_no_header, data) + + read_rows, header = csv_to_records(test_path, header_row=True) + self.assert_equal_arr(header, data[0]) + self.assert_equal_arr(read_rows, [data[1], data[2]]) diff --git a/release_scripts/localization_scripts/test/test_fileutil.py b/release_scripts/localization_scripts/test/test_fileutil.py new file mode 100644 index 0000000000..290396eba7 --- /dev/null +++ b/release_scripts/localization_scripts/test/test_fileutil.py @@ -0,0 +1,52 @@ +import os +import unittest +from typing import Tuple +from pathlib import Path +from fileutil import get_path_pieces, get_new_path, get_filename_addition + + +def joined_paths(pieces: Tuple[str, str, str]) -> str: + return os.path.join(pieces[0], pieces[1] + '.' + pieces[2]) + + +PATH_PIECES1 = ('/test/folder', 'filename', 'ext') +PATH_PIECES2 = ('/test.test2/folder.test2', 'filename.test', 'ext') +PATH_PIECES3 = ('/test.test2/folder.test2/folder', None, None) + +PATH1 = joined_paths(PATH_PIECES1) +PATH2 = joined_paths(PATH_PIECES2) +PATH3 = PATH_PIECES3[0] + +ALL_ITEMS = [ + (PATH_PIECES1, PATH1), + (PATH_PIECES2, PATH2), + (PATH_PIECES3, PATH3) +] + + +class FileUtilTest(unittest.TestCase): + def test_get_path_pieces(self): + for (expected_path, expected_filename, expected_ext), path in ALL_ITEMS: + path, filename, ext = get_path_pieces(path) + self.assertEqual(path, str(Path(expected_path))) + self.assertEqual(filename, expected_filename) + self.assertEqual(ext, expected_ext) + + def test_get_new_path(self): + for (expected_path, expected_filename, expected_ext), path in ALL_ITEMS: + new_name = "newname.file" + new_path = get_new_path(path, new_name) + self.assertEqual(new_path, str(Path(expected_path) / Path(new_name))) + + def test_get_filename_addition(self): + for (expected_path, expected_filename, expected_ext), path in ALL_ITEMS: + addition = "addition" + new_path = get_filename_addition(path, addition) + if expected_filename is None or expected_ext is None: + expected_file_path = Path(expected_path + addition) + else: + expected_file_path = Path(expected_path) / Path("{file_name}{addition}.{extension}".format( + file_name=expected_filename, addition=addition, extension=expected_ext)) + + self.assertEqual( + new_path, str(expected_file_path)) diff --git a/release_scripts/localization_scripts/test/test_itemchange.py b/release_scripts/localization_scripts/test/test_itemchange.py new file mode 100644 index 0000000000..91b7846a11 --- /dev/null +++ b/release_scripts/localization_scripts/test/test_itemchange.py @@ -0,0 +1,96 @@ +import unittest +from typing import Dict + +from itemchange import get_changed, ChangeType + + +def dict_to_prop_str(this_dict: Dict[str, str]) -> str: + toret = '' + for key, val in this_dict.items(): + toret += "{key}={value}\n".format(key=key, value=val) + + return toret + + +class ItemChangeTest(unittest.TestCase): + def test_get_changed(self): + deleted_key = 'deleted.property.key' + deleted_val = 'will be deleted' + + change_key = 'change.property.key' + change_val_a = 'original value' + change_val_b = 'new value' + + change_key2 = 'change2.property.key' + change_val2_a = 'original value 2' + change_val2_b = '' + + change_key3 = 'change3.property.key' + change_val3_a = '' + change_val3_b = 'cur value 3' + + addition_key = 'addition.property.key' + addition_new_val = 'the added value' + + same_key = 'samevalue.property.key' + same_value = 'the same value' + + same_key2 = 'samevalue2.property.key' + same_value2 = '' + + a_dict = { + deleted_key: deleted_val, + change_key: change_val_a, + change_key2: change_val2_a, + change_key3: change_val3_a, + same_key: same_value, + same_key2: same_value2 + } + + b_dict = { + change_key: change_val_b, + change_key2: change_val2_b, + change_key3: change_val3_b, + addition_key: addition_new_val, + same_key: same_value, + same_key2: same_value2 + } + + a_str = dict_to_prop_str(a_dict) + b_str = dict_to_prop_str(b_dict) + + rel_path = 'my/rel/path.properties' + + key_to_change = {} + + for item_change in get_changed(rel_path, a_str, b_str): + self.assertEqual(item_change.rel_path, rel_path) + key_to_change[item_change.key] = item_change + + deleted_item = key_to_change[deleted_key] + self.assertEqual(deleted_item.type, ChangeType.DELETION) + self.assertEqual(deleted_item.prev_val, deleted_val) + self.assertEqual(deleted_item.cur_val, None) + + addition_item = key_to_change[addition_key] + self.assertEqual(addition_item.type, ChangeType.ADDITION) + self.assertEqual(addition_item.prev_val, None) + self.assertEqual(addition_item.cur_val, addition_new_val) + + change_item = key_to_change[change_key] + self.assertEqual(change_item.type, ChangeType.CHANGE) + self.assertEqual(change_item.prev_val, change_val_a) + self.assertEqual(change_item.cur_val, change_val_b) + + change_item2 = key_to_change[change_key2] + self.assertEqual(change_item2.type, ChangeType.CHANGE) + self.assertEqual(change_item2.prev_val, change_val2_a) + self.assertEqual(change_item2.cur_val, change_val2_b) + + change_item3 = key_to_change[change_key3] + self.assertEqual(change_item3.type, ChangeType.CHANGE) + self.assertEqual(change_item3.prev_val, change_val3_a) + self.assertEqual(change_item3.cur_val, change_val3_b) + + self.assertTrue(same_key not in key_to_change) + self.assertTrue(same_key2 not in key_to_change) diff --git a/release_scripts/localization_scripts/test/test_propsutil.py b/release_scripts/localization_scripts/test/test_propsutil.py new file mode 100644 index 0000000000..f69129399a --- /dev/null +++ b/release_scripts/localization_scripts/test/test_propsutil.py @@ -0,0 +1,36 @@ +import os +import unittest + +from propsutil import set_entry_dict, get_entry_dict_from_path, update_entry_dict +from test.unittestutil import get_output_path + + +class PropsUtilTest(unittest.TestCase): + def test_update_entry_dict(self): + orig_key = 'orig_key' + orig_val = 'orig_val 片仮å ' + to_be_altered_key = 'tobealteredkey' + first_val = 'not yet altered sábado' + second_val = 'altered Stöcke' + + orig_props = { + orig_key: orig_val, + to_be_altered_key: first_val + } + + update_props = { + to_be_altered_key: second_val + } + + os.makedirs(get_output_path(), exist_ok=True) + test_path = get_output_path('test.props') + set_entry_dict(orig_props, test_path) + + orig_read_props = get_entry_dict_from_path(test_path) + self.assertEqual(orig_read_props[orig_key], orig_val) + self.assertEqual(orig_read_props[to_be_altered_key], first_val) + + update_entry_dict(update_props, test_path) + updated_read_props = get_entry_dict_from_path(test_path) + self.assertEqual(updated_read_props[orig_key], orig_val) + self.assertEqual(updated_read_props[to_be_altered_key], second_val) diff --git a/release_scripts/localization_scripts/test/unittestutil.py b/release_scripts/localization_scripts/test/unittestutil.py new file mode 100644 index 0000000000..19face5610 --- /dev/null +++ b/release_scripts/localization_scripts/test/unittestutil.py @@ -0,0 +1,14 @@ +import os +from typing import Union + +from envutil import get_proj_dir + +TEST_ARTIFACT_FOLDER = 'artifacts' +TEST_OUTPUT_FOLDER = 'output' + + +def get_output_path(filename: Union[str, None] = None) -> str: + if filename is None: + return os.path.join(get_proj_dir(__file__), TEST_ARTIFACT_FOLDER, TEST_OUTPUT_FOLDER) + else: + return os.path.join(get_proj_dir(__file__), TEST_ARTIFACT_FOLDER, TEST_OUTPUT_FOLDER, filename) diff --git a/release_scripts/localization_scripts/updatepropsscript.py b/release_scripts/localization_scripts/updatepropsscript.py new file mode 100644 index 0000000000..8ba02d4664 --- /dev/null +++ b/release_scripts/localization_scripts/updatepropsscript.py @@ -0,0 +1,262 @@ +"""This script finds all '.properties-MERGED' files and writes relative path, key, and value to a CSV file. +This script requires the python libraries: jproperties. It also requires Python 3.x. +""" + +from typing import List, Dict, Tuple, Callable, Iterator +import sys +import os + +from envutil import get_proj_dir +from fileutil import get_new_path +from gitutil import get_git_root +from langpropsutil import set_commit_for_language +from propsutil import set_entry_dict, get_entry_dict_from_path, get_lang_bundle_name +from csvutil import csv_to_records +from propentry import PropEntry +import argparse + + +def write_prop_entries(entries: Iterator[PropEntry], repo_path: str): + """Writes property entry items to their expected relative path within the repo path. + Previously existing files will be overwritten and prop entries marked as should_be_deleted will + not be included. + + Args: + entries (List[PropEntry]): the prop entry items to write to disk. + repo_path (str): The path to the git repo. + """ + items_by_file = get_by_file(entries) + for rel_path, (entries, ignored) in items_by_file.items(): + abs_path = os.path.join(repo_path, rel_path) + set_entry_dict(entries, abs_path) + + +def update_prop_entries(entries: Iterator[PropEntry], repo_path: str): + """Updates property entry items to their expected relative path within the repo path. The union of + entries provided and any previously existing entries will be created. Keys marked for deletion will be + removed from the generated property files. + + Args: + entries (List[PropEntry]): the prop entry items to write to disk. + repo_path (str): The path to the git repo. + """ + items_by_file = get_by_file(entries) + for rel_path, (entries, to_delete) in items_by_file.items(): + abs_path = os.path.join(repo_path, rel_path) + + prop_items = get_entry_dict_from_path(abs_path) + if prop_items is None: + prop_items = {} + + for key_to_delete in to_delete: + if key_to_delete in prop_items: + del prop_items[key_to_delete] + + for key, val in entries.items(): + prop_items[key] = val + + set_entry_dict(prop_items, abs_path) + + +def get_by_file(entries: Iterator[PropEntry]) -> Dict[str, Tuple[Dict[str, str], List[str]]]: + """Sorts a prop entry list by file. The return type is a dictionary mapping + the file path to a tuple containing the key-value pairs to be updated and a + list of keys to be deleted. + + Args: + entries (List[PropEntry]): The entries to be sorted. + + Returns: + Dict[str, Tuple[Dict[str,str], List[str]]]: A dictionary mapping + the file path to a tuple containing the key-value pairs to be updated and a + list of keys to be deleted. + """ + to_ret = {} + for prop_entry in entries: + rel_path = prop_entry.rel_path + key = prop_entry.key + value = prop_entry.value + + if rel_path not in to_ret: + to_ret[rel_path] = ({}, []) + + if prop_entry.should_delete: + to_ret[rel_path][1].append(prop_entry.key) + else: + to_ret[rel_path][0][key] = value + + return to_ret + + +def idx_bounded(num: int, max_exclusive: int) -> bool: + return 0 <= num < max_exclusive + + +def get_prop_entry(row: List[str], + path_idx: int = 0, + key_idx: int = 1, + value_idx: int = 2, + should_delete_converter: Callable[[List[str]], bool] = None, + path_converter: Callable[[str], str] = None) -> PropEntry: + """Parses a PropEntry object from a row of values in a csv. + + Args: + row (List[str]): The csv file row to parse. + path_idx (int, optional): The column index for the relative path of the properties file. Defaults to 0. + key_idx (int, optional): The column index for the properties key. Defaults to 1. + value_idx (int, optional): The column index for the properties value. Defaults to 2. + should_delete_converter (Callable[[List[str]], bool], optional): If not None, this determines if the key should + be deleted from the row values. Defaults to None. + path_converter (Callable[[str], str], optional): If not None, this determines the relative path to use in the + created PropEntry given the original relative path. Defaults to None. + + Returns: + PropEntry: The generated prop entry object. + """ + + path = row[path_idx] if idx_bounded(path_idx, len(row)) else None + if path_converter is not None: + path = path_converter(path) + + key = row[key_idx] if idx_bounded(key_idx, len(row)) else None + value = row[value_idx] if idx_bounded(value_idx, len(row)) else None + should_delete = False if should_delete_converter is None else should_delete_converter(row) + return PropEntry(path, key, value, should_delete) + + +def get_prop_entries(rows: List[List[str]], + path_idx: int = 0, + key_idx: int = 1, + value_idx: int = 2, + should_delete_converter: Callable[[List[str]], bool] = None, + path_converter: Callable[[str], str] = None) -> Iterator[PropEntry]: + + """Parses PropEntry objects from rows of values in a csv. Any items that have an empty string value will be + ignored. + + Args: + rows (List[List[str]]): The csv file rows to parse. + path_idx (int, optional): The column index for the relative path of the properties file. Defaults to 0. + key_idx (int, optional): The column index for the properties key. Defaults to 1. + value_idx (int, optional): The column index for the properties value. Defaults to 2. + should_delete_converter (Callable[[List[str]], bool], optional): If not None, this determines if the key should + be deleted from the row values. Defaults to None. + path_converter (Callable[[str], str], optional): If not None, this determines the relative path to use in the + created PropEntry given the original relative path. Defaults to None. + + Returns: + List[PropEntry]: The generated prop entry objects. + """ + propentry_iter = map(lambda row: get_prop_entry(row, path_idx, key_idx, value_idx, should_delete_converter, + path_converter), rows) + + # filter rows that have no value + return filter(lambda entry: entry and entry.value.strip(), propentry_iter) + + +def get_should_deleted(row_items: List[str], requested_idx: int) -> bool: + """If there is a value at row_items[requested_idx] and that value starts with 'DELET', then this will return true. + + Args: + row_items (List[str]): The row items. + requested_idx (int): The index specifying if the property should be deleted. + + Returns: + bool: True if the row specifies it should be deleted. + """ + if idx_bounded(requested_idx, len(row_items)) and row_items[requested_idx].strip().upper().startswith('DELET'): + return True + else: + return False + + +def main(): + # noinspection PyTypeChecker + parser = argparse.ArgumentParser(description='Updates properties files in the autopsy git repo.', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument(dest='csv_file', type=str, help='The path to the csv file. The default format for the csv ' + 'file has columns of relative path, properties file key, ' + 'properties file value, whether or not the key should be ' + 'deleted, and commit id for how recent these updates are. ' + 'If the key should be deleted, the deletion row should be ' + '\'DELETION.\' A header row is expected by default and the ' + 'commit id, if specified, should only be in the first row. The' + ' input path should be specified as a relative path with the ' + 'dot slash notation (i.e. `./inputpath.csv`) or an absolute ' + 'path.') + + parser.add_argument('-r', '--repo', dest='repo_path', type=str, required=False, + help='The path to the repo. If not specified, parent repo of path of script is used.') + parser.add_argument('-p', '--path-idx', dest='path_idx', action='store', type=int, default=0, required=False, + help='The column index in the csv file providing the relative path to the properties file.') + parser.add_argument('-k', '--key-idx', dest='key_idx', action='store', type=int, default=1, required=False, + help='The column index in the csv file providing the key within the properties file.') + parser.add_argument('-v', '--value-idx', dest='value_idx', action='store', type=int, default=2, required=False, + help='The column index in the csv file providing the value within the properties file.') + parser.add_argument('-d', '--should-delete-idx', dest='should_delete_idx', action='store', type=int, default=3, + required=False, help='The column index in the csv file providing whether or not the file ' + 'should be deleted. Any non-blank content will be treated as True.') + parser.add_argument('-c', '--commit-idx', dest='latest_commit_idx', action='store', type=int, default=4, + required=False, help='The column index in the csv file providing the commit for which this ' + 'update applies. The commit should be located in the header row. ') + + parser.add_argument('-f', '--file-rename', dest='file_rename', action='store', type=str, default=None, + required=False, help='If specified, the properties file will be renamed to the argument' + ' preserving the specified relative path.') + parser.add_argument('-z', '--has-no-header', dest='has_no_header', action='store_true', default=False, + required=False, help='Specify whether or not there is a header within the csv file.') + parser.add_argument('-o', '--should-overwrite', dest='should_overwrite', action='store_true', default=False, + required=False, help="Whether or not to overwrite the previously existing properties files" + " ignoring previously existing values.") + + parser.add_argument('-l', '--language', dest='language', type=str, default='HEAD', required=False, + help='Specify the language in order to update the last updated properties file and rename ' + 'files within directories. This flag overrides the file-rename flag.') + + args = parser.parse_args() + + repo_path = args.repo_path if args.repo_path is not None else get_git_root(get_proj_dir()) + input_path = args.csv_file + path_idx = args.path_idx + key_idx = args.key_idx + value_idx = args.value_idx + has_header = not args.has_no_header + overwrite = args.should_overwrite + + # means of determining if a key should be deleted from a file + if args.should_delete_idx is None: + should_delete_converter = None + else: + def should_delete_converter(row_items: List[str]): + return get_should_deleted(row_items, args.should_delete_idx) + + # provides the means of renaming the bundle file + if args.language is not None: + def path_converter(orig_path: str): + return get_new_path(orig_path, get_lang_bundle_name(args.language)) + elif args.file_rename is not None: + def path_converter(orig_path: str): + return get_new_path(orig_path, args.file_rename) + else: + path_converter = None + + # retrieve records from csv + all_items, header = list(csv_to_records(input_path, has_header)) + prop_entries = get_prop_entries(all_items, path_idx, key_idx, value_idx, should_delete_converter, path_converter) + + # write to files + if overwrite: + write_prop_entries(prop_entries, repo_path) + else: + update_prop_entries(prop_entries, repo_path) + + # update the language last update if applicable + if args.language is not None and header is not None and len(header) > args.latest_commit_idx >= 0: + set_commit_for_language(args.language, header[args.latest_commit_idx]) + + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/release_scripts/update_sleuthkit_version.pl b/release_scripts/update_sleuthkit_version.pl index 26d8be5073..1d6bdc7e72 100755 --- a/release_scripts/update_sleuthkit_version.pl +++ b/release_scripts/update_sleuthkit_version.pl @@ -131,10 +131,14 @@ sub update_core_project_properties { my $found = 0; while () { - if (/^file\.reference\.sleuthkit\-/) { + if (/^file\.reference\.sleuthkit\-4/) { print CONF_OUT "file.reference.sleuthkit-${VER}.jar=release/modules/ext/sleuthkit-${VER}.jar\n"; $found++; } + elsif (/^file\.reference\.sleuthkit\-caseuco-4/) { + print CONF_OUT "file.reference.sleuthkit-caseuco-${VER}.jar=release/modules/ext/sleuthkit-caseuco-${VER}.jar\n"; + $found++; + } else { print CONF_OUT $_; @@ -143,8 +147,8 @@ sub update_core_project_properties { close (CONF_IN); close (CONF_OUT); - if ($found != 1) { - die "$found (instead of 1) occurrences of version found in ${orig}"; + if ($found != 2) { + die "$found (instead of 2) occurrences of version found in core ${orig}"; } unlink ($orig) or die "Error deleting ${orig}"; @@ -167,14 +171,22 @@ sub update_core_project_xml { my $found = 0; while () { - if (/ext\/sleuthkit-/) { + if (/ext\/sleuthkit-4/) { print CONF_OUT " ext/sleuthkit-${VER}.jar\n"; $found++; } - elsif (/release\/modules\/ext\/sleuthkit-/) { + elsif (/release\/modules\/ext\/sleuthkit-4/) { print CONF_OUT " release/modules/ext/sleuthkit-${VER}.jar\n"; $found++; } + elsif (/ext\/sleuthkit-caseuco-4/) { + print CONF_OUT " ext/sleuthkit-caseuco-${VER}.jar\n"; + $found++; + } + elsif (/release\/modules\/ext\/sleuthkit-caseuco-4/) { + print CONF_OUT " release/modules/ext/sleuthkit-caseuco-${VER}.jar\n"; + $found++; + } else { print CONF_OUT $_; } @@ -182,8 +194,8 @@ sub update_core_project_xml { close (CONF_IN); close (CONF_OUT); - if ($found != 2) { - die "$found (instead of 2) occurrences of version found in ${orig}"; + if ($found != 4) { + die "$found (instead of 4) occurrences of version found in case ${orig}"; } unlink ($orig) or die "Error deleting ${orig}"; diff --git a/test/script/regression.py b/test/script/regression.py index a36929491c..9999a5c563 100644 --- a/test/script/regression.py +++ b/test/script/regression.py @@ -1074,7 +1074,7 @@ class TestResultsDiffer(object): # Ensure gold is passed before output (subprocess.check_output(["diff", '-r', '-N', '-x', '*.png', '-x', '*.ico', '--ignore-matching-lines', 'HTML Report Generated on \|Autopsy Report for case \|Case:\|Case Number:' - '\|Examiner:', gold_report_path, output_report_path])) + '\|Examiner:\|Unalloc_', gold_report_path, output_report_path])) print_report("", "REPORT COMPARISON", "The test html reports matched the gold reports") return True except subprocess.CalledProcessError as e: diff --git a/test/script/tskdbdiff.py b/test/script/tskdbdiff.py index baab8573df..f152cd923a 100644 --- a/test/script/tskdbdiff.py +++ b/test/script/tskdbdiff.py @@ -208,10 +208,12 @@ class TskDbDiff(object): while (row != None): # File Name and artifact type + # Remove parent object ID from Unalloc file name + normalizedName = re.sub('^Unalloc_[0-9]+_', 'Unalloc_', row["name"]) if(row["parent_path"] != None): - database_log.write(row["parent_path"] + row["name"] + ' ') + database_log.write(row["parent_path"] + normalizedName + ' ') else: - database_log.write(row["name"] + ' ') + database_log.write(normalizedName + ' ') if isMultiUser: attribute_cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) @@ -427,6 +429,7 @@ def normalize_db_entry(line, files_table, vs_parts_table, vs_info_table, fs_info files_index = line.find('INSERT INTO "tsk_files"') > -1 or line.find('INSERT INTO tsk_files ') > -1 path_index = line.find('INSERT INTO "tsk_files_path"') > -1 or line.find('INSERT INTO tsk_files_path ') > -1 object_index = line.find('INSERT INTO "tsk_objects"') > -1 or line.find('INSERT INTO tsk_objects ') > -1 + vs_parts_index = line.find('INSERT INTO "tsk_vs_parts"') > -1 or line.find('INSERT INTO tsk_vs_parts ') > -1 report_index = line.find('INSERT INTO "reports"') > -1 or line.find('INSERT INTO reports ') > -1 layout_index = line.find('INSERT INTO "tsk_file_layout"') > -1 or line.find('INSERT INTO tsk_file_layout ') > -1 data_source_info_index = line.find('INSERT INTO "data_source_info"') > -1 or line.find('INSERT INTO data_source_info ') > -1 @@ -462,6 +465,12 @@ def normalize_db_entry(line, files_table, vs_parts_table, vs_info_table, fs_info # remove object ID if files_index: newLine = ('INSERT INTO "tsk_files" VALUES(' + ', '.join(fields_list[1:]) + ');') + # Remove object ID from Unalloc file name + newLine = re.sub('Unalloc_[0-9]+_', 'Unalloc_', newLine) + return newLine + # remove object ID + elif vs_parts_index: + newLine = ('INSERT INTO "tsk_vs_parts" VALUES(' + ', '.join(fields_list[1:]) + ');') return newLine # remove group ID elif ig_groups_index: @@ -496,7 +505,9 @@ def normalize_db_entry(line, files_table, vs_parts_table, vs_info_table, fs_info elif layout_index: obj_id = fields_list[0] path= files_table[int(obj_id)] - newLine = ('INSERT INTO "tsk_file_layout" VALUES(' + path + ', ' + ', '.join(fields_list[1:]) + ');') + newLine = ('INSERT INTO "tsk_file_layout" VALUES(' + path + ', ' + ', '.join(fields_list[1:]) + ');') + # Remove object ID from Unalloc file name + newLine = re.sub('Unalloc_[0-9]+_', 'Unalloc_', newLine) return newLine # remove object ID elif object_index: @@ -557,6 +568,11 @@ def normalize_db_entry(line, files_table, vs_parts_table, vs_info_table, fs_info parent_path = parent_path[parent_path.find('ModuleOutput'):] if path and parent_path: + # Remove object ID from Unalloc file names and regripper output + path = re.sub('Unalloc_[0-9]+_', 'Unalloc_', path) + path = re.sub('regripper\-[0-9]+\-full', 'regripper-full', path) + parent_path = re.sub('Unalloc_[0-9]+_', 'Unalloc_', parent_path) + parent_path = re.sub('regripper\-[0-9]+\-full', 'regripper-full', parent_path) return newLine + path + ', ' + parent_path + ', ' + ', '.join(fields_list[2:]) + ');' else: return line diff --git a/thirdparty/InterestingFileSetRules/Cloud Storage.xml b/thirdparty/InterestingFileSetRules/Cloud Storage.xml new file mode 100644 index 0000000000..8e900f8c9a --- /dev/null +++ b/thirdparty/InterestingFileSetRules/Cloud Storage.xml @@ -0,0 +1,37 @@ + + + + creative cloud.exe + amazonphotos.exe + box.exe + carboniteUI.exe + CloudScheduler.exe + CloudMe.exe + dropbox.exe + DropboxUniversal.exe + efcClient.exe + GigaTribe.exe + goodsync.exe + googledrivesync.exe + GoogleDriveFS.exe + iCloudDrive.exe + MegaApp.exe + MEGAsync.exe + nextcloud.exe + onedrive.exe + microsoft.microsoftskydrive.exe + owncloud.exe + pcloud.exe + Resilio Sync.exe + seafile-applet.exe + slack.exe + SpiderOakONE.exe + sugarsync.exe + sync-taskbar.exe + Syncthing.exe + SyncTrazor.exe + synqion.exe + yandexdisk2.exe + zohodocs.exe + + diff --git a/thirdparty/InterestingFileSetRules/Cryptocurrency Wallets.xml b/thirdparty/InterestingFileSetRules/Cryptocurrency Wallets.xml new file mode 100644 index 0000000000..fb7aaa1204 --- /dev/null +++ b/thirdparty/InterestingFileSetRules/Cryptocurrency Wallets.xml @@ -0,0 +1,44 @@ + + + + atomic wallet.exe + armoryqt.exe + bitcoin-qt.exe + BitherWinLauncher.exe + bitpay.exe + canoe.exe + coinomi.exe + copay.exe + dash-qt.exe + ^electrum-dash(.*).exe$ + dogecoin-qt.exe + eidoo.exe + electron-cash.exe + ^electron-cash(.*)portable.exe$ + ^electron-cash(.*).exe$ + ^electrum(.*).exe + ^electrum(.*)portable.exe + exodus.exe + GreenAddress Wallet.exe + guarda.exe + jaxx liberty.exe + Ledger Live.exe + lisk.exe + litecoin-qt.exe + monero-wallet-gui.exe + multibit-hd.exe + multidoge.exe + NanoVault.exe + neon.exe + qtum-qt.exe + ^Qtum-electrum-win-(.*).exe$ + stargazer.exe + toastwallet.exe + trezord.exe + verge-qt.exe + Zecwallet Fullnode.exe + Zecwallet Lite.exe + zelcore.exe + zelcore-portable.exe + + diff --git a/thirdparty/InterestingFileSetRules/Encryption Programs.xml b/thirdparty/InterestingFileSetRules/Encryption Programs.xml new file mode 100644 index 0000000000..5ad59eee3b --- /dev/null +++ b/thirdparty/InterestingFileSetRules/Encryption Programs.xml @@ -0,0 +1,25 @@ + + + + aescrypt.exe + AxCrypt.exe + BitLockerDeviceEncryption.exe + certainsafe.exe + cexpertcmd.exe + cexpert_gui.exe + Cryptomator.exe + ecryptfs + EncFSMP.exe + Encrypto.exe + Folder Lock.exe + GFileEncryption.exe + gpg.exe + gdbus.exe + KeePass.exe + luks + Desktop-Bridge.exe + Tor Browser + Tutanota Desktop.exe + VeraCrypt.exe + + diff --git a/thirdparty/InterestingFileSetRules/Privacy Programs.xml b/thirdparty/InterestingFileSetRules/Privacy Programs.xml new file mode 100644 index 0000000000..257ffa00e2 --- /dev/null +++ b/thirdparty/InterestingFileSetRules/Privacy Programs.xml @@ -0,0 +1,50 @@ + + + + AncorFreeHotspotShieldVPN.exe + AncorFreeVPN.exe + HotspotShieldVPN.EXE + AVGBrowser.exe + Avira.Systray.exe + AviraPhantomVPN.exe + AviraScout.exe + Brave.exe + CM.exe + dragon.exe + CyberGhost.exe + Disconnect.exe + DolphinZero.exe + Dooble.exe + Epic.exe + ExpressVPN.exe + Freelan.exe + Freenet.exe + HidemeVPN.exe + Hideme.exe + HideMyAss.exe + HotspotShield.exe + HTTPSEverywhere.exe + KasperskyVPN.exe + NordVPN.exe + NortonSecureVpn.exe + NortonVPN.exe + OpenVpn.exe + PaleMoon.exe + PIA.exe + PrivateInternetAccess.exe + PrivateVPN.exe + ProtonVPN.exe + PureVPN.exe + SaferVPN.exe + SRWareIron.exe + SurfShark.exe + Tor.exe + Tunnelbear.exe + UltraVpn.exe + VpnGate.exe + VyprVPN.exe + Waterfox.exe + ZenMate.exe + Zenmate.exe + + diff --git a/thirdparty/OfficialHashSets/README.txt b/thirdparty/OfficialHashSets/README.txt new file mode 100644 index 0000000000..3ea1ac5801 --- /dev/null +++ b/thirdparty/OfficialHashSets/README.txt @@ -0,0 +1 @@ +.kdb files can be placed in this directory and they will be treated as Official Hash Sets within autopsy. Official Hash Sets will be readonly within the application. Official Hash Sets should have the following naming convention: '..kdb' where '' is the name of the Official Hash Set and '' is the known status of the hash set. The known status is the identifier for one of the variable names represented in the enum: HashDbManager.HashDb.KnownFilesType. As an example, a possible value could be: 'Foo.Notable.kdb' where the name of the Official Hash Set is 'Foo' and the known status is 'Notable' which is the identifier for KnownFilesType.KNOWN_BAD. \ No newline at end of file diff --git a/thirdparty/markmckinnon/Export_Srudb_Linux b/thirdparty/markmckinnon/Export_Srudb_Linux old mode 100644 new mode 100755 diff --git a/thirdparty/markmckinnon/Export_srudb_macos b/thirdparty/markmckinnon/Export_srudb_macos old mode 100644 new mode 100755 diff --git a/thirdparty/markmckinnon/parse_prefetch_linux b/thirdparty/markmckinnon/parse_prefetch_linux new file mode 100755 index 0000000000..183674e9cf Binary files /dev/null and b/thirdparty/markmckinnon/parse_prefetch_linux differ diff --git a/thirdparty/markmckinnon/parse_prefetch_macos b/thirdparty/markmckinnon/parse_prefetch_macos new file mode 100755 index 0000000000..f36fa57d09 Binary files /dev/null and b/thirdparty/markmckinnon/parse_prefetch_macos differ diff --git a/thirdparty/markmckinnon/parse_prefetch_x64.exe b/thirdparty/markmckinnon/parse_prefetch_x64.exe new file mode 100644 index 0000000000..3e0b7ae674 Binary files /dev/null and b/thirdparty/markmckinnon/parse_prefetch_x64.exe differ diff --git a/thirdparty/markmckinnon/parse_prefetch_x86.exe b/thirdparty/markmckinnon/parse_prefetch_x86.exe new file mode 100644 index 0000000000..fb58178c3a Binary files /dev/null and b/thirdparty/markmckinnon/parse_prefetch_x86.exe differ diff --git a/thirdparty/photorec_exec/63/cygwin b/thirdparty/photorec_exec/64-bit/bin/63/cygwin similarity index 100% rename from thirdparty/photorec_exec/63/cygwin rename to thirdparty/photorec_exec/64-bit/bin/63/cygwin diff --git a/thirdparty/photorec_exec/AUTHORS.txt b/thirdparty/photorec_exec/64-bit/bin/AUTHORS.txt similarity index 100% rename from thirdparty/photorec_exec/AUTHORS.txt rename to thirdparty/photorec_exec/64-bit/bin/AUTHORS.txt diff --git a/thirdparty/photorec_exec/COPYING.txt b/thirdparty/photorec_exec/64-bit/bin/COPYING.txt similarity index 100% rename from thirdparty/photorec_exec/COPYING.txt rename to thirdparty/photorec_exec/64-bit/bin/COPYING.txt diff --git a/thirdparty/photorec_exec/64-bit/bin/INFO b/thirdparty/photorec_exec/64-bit/bin/INFO new file mode 100644 index 0000000000..da71e56064 --- /dev/null +++ b/thirdparty/photorec_exec/64-bit/bin/INFO @@ -0,0 +1,2 @@ +TestDisk & PhotoRec , https://www.cgsecurity.org +Copyright (C) 1998-2019 Christophe GRENIER diff --git a/thirdparty/photorec_exec/NEWS.txt b/thirdparty/photorec_exec/64-bit/bin/NEWS.txt similarity index 91% rename from thirdparty/photorec_exec/NEWS.txt rename to thirdparty/photorec_exec/64-bit/bin/NEWS.txt index de58defb27..263557d826 100644 --- a/thirdparty/photorec_exec/NEWS.txt +++ b/thirdparty/photorec_exec/64-bit/bin/NEWS.txt @@ -1,4 +1,44 @@ Current news +== 7.1 == +== General Improvements == +* It should be possible to reproduce, byte for byte, every build of testdisk package in Debian https://wiki.debian.org/ReproducibleBuilds +* Windows: Identify again the device model (Regression present in 7.0 version) + +== TestDisk == +=== Improvements === +* Ask confirmation when user quits the list of partitions found by TestDisk +=== Bug fixes === +* Various fix for Scripted run + +== PhotoRec & QPhotoRec == +=== Improvements === +* Log the correct filenames in photorec.log file +* Reduced false positives for tar +* Fix recovery of mobius camera videos. +* Fix recovery of progressive jpeg +* Extract the filesize from sqlite header when available +* Recover more fragmented files + +Extract of the new file formats recovered by PhotoRec: +* .aes: Synology AES key files +* .axp: Pinnacle Studio +* .bdm: AVHCD index +* .cpi: AVCHD Clip Information +* .DS_Store: Apple Desktop Services Store +* .dvr: RT60 +* .gi: Roxio Creator +* .gpx: Guitar Pro 6 +* .gp4: Guitar Pro 4 +* .iam/.ipt Autodesk Inventor part +* .icns: Apple Icon Image +* .jsonlz4: Mozilla bookmarks +* .lyx LyX 2.X +* .mpl: AVHCD playlist +* .tg Tux Guitar 1.2 +* .vdj: VirtualDJ +* .wee: weecast +* .xar: XAR Archive + == 7.0 == === General Improvements === @@ -96,14 +136,14 @@ New file formats: == 6.13 == -Fix UAC manifests for Windows, so users don't need to use right-click "Run As Administrator" +Fix UAC manifests for Windows, so users don't need to use right-click "Run As Administrator" TestDisk - Fix image creation, image.dd file wasn't created (Regression introduced in 6.12) - Detect Vmware VMFS partition - Locate lost GFS2 partition but not yet the size - Log HDD serial number and firmware revision -- List NTFS Alternate Data Streams (ADS) +- List NTFS Alternate Data Streams (ADS) PhotoRec - Session recovery restarts at the previous location @@ -111,13 +151,13 @@ PhotoRec - Better JPG recovery, there should be less cases where thumbnails were recovered instead of the picture itself. - Handle large avi files using "AVIX" or mov files using 64-bit chunk size. - Rename recovered pdf using the title (not perfect) -- Major cleanup of PhotoRec core code +- Major cleanup of PhotoRec core code 6.12 fidentify, a little utility sharing PhotoRec signature database, is now build by default. It identifies the type of data contained in a file and reports the extension as seen by PhotoRec. It is similar to the Unix file command. Add compatibility with - libewf 20110312 - ncurses 5.8 -- ntfs-3g +- ntfs-3g Fix detection of Encase 6 .ewf files TestDisk @@ -125,14 +165,14 @@ TestDisk - Better HPA/DCO detection: handle the case where native_max is null. - Image Creation is now faster than previous version when there are bad sectors - List and copy (experimental) files from exFAT filesystem -- Improved NTFS undelete interface +- Improved NTFS undelete interface PhotoRec - PhotoRec checks for EFBIG (file too large) error when writing files. It's usefull to avoid erronous message about "no free space available" when recovering to a FAT filesystem. - Recover files from exFAT unallocated space - Use doc/xls/ppt title to name recovered Office document, use first filename in zip archive... - Possibility to add your own extension/custom signature to PhotoRec -- Generated a report.xml file using Digital Forensics XML +- Generated a report.xml file using Digital Forensics XML 6.11 TestDisk & PhotoRec 6.11 should use less CPU. @@ -172,7 +212,7 @@ MacBook and Mac Pro. be enabled for at least MacOSX, so users won't have to go into a command line. - Improved Windows disk support, most internal USB card reader should now work. - Disk model (ie. ATA ST3120026AS, _NEC DVD_RW ND-4550A...) are now reported -under Linux. +under Linux. TestDisk - New file system support has been added: encrypted LUKS, Mac HFSX, Linux Raid @@ -241,13 +281,13 @@ InDesign .indd, Matroska .mkv, MP3 with ID3 header, MS cabinet archive .cab, MSOffice "Open" XML .docx, .pptx, .xlsx, MS executable (PE), MS Windows Metafile .wmf, NJStar .njx, Quickbooks .qbb and .qbw, Real Audio .rm, registry config file .reg, RPM package .rpm, Windows registry header -detection and Event Log .evt +detection and Event Log .evt 6.6 General Improvements - Encase Expert Witness Compression Format is now supported, so Computer Forensic Experts can use TestDisk and PhotoRec more easily. -- Under new Vista OS, harddisks are now being reported again. +- Under new Vista OS, harddisks are now being reported again. TestDisk - If LBA48 support seems missing (HD bigger than 130 GB not supported by the OS), TestDisk will warn the user. This problem is common when a user @@ -256,7 +296,7 @@ reinstalls his OS and forgets to install the latest service patches. some filesystem corruption has occurred; more importantly, it will allow the user to copy whatever file data it can as well. - FAT: Improved heuristics to find the first FAT area during boot sector -rebuild. +rebuild. PhotoRec - A new method for handling fragmented data is now used, making recovery more reliable and faster. @@ -267,7 +307,7 @@ efficient. - New file formats have been added: .3g2 (Mov video family), .aif Apple Audio, .all .cpr Cubase Song, .blend Blender, .cam Casio QV Digital Camera Image, .flac Free Lossless Audio Codec, .mdf Microsoft SQL, .swf -Macromedia Flash and .vcf VCard (not confused with .txt anymore) +Macromedia Flash and .vcf VCard (not confused with .txt anymore) 6.5 TestDisk @@ -280,7 +320,7 @@ partition to a selected directory. PhotoRec - New file formats have been added: AppleWorks .cwk, DIF Digital Video .dv, DjVu .djv, Finale .mus, Incredimail .imm, .imb, iTunes mhbd, MIDI .mid, -MS Backup, Real Media .rm & .ram, Reason .rns, ruby .rb, .xml +MS Backup, Real Media .rm & .ram, Reason .rns, ruby .rb, .xml - File size detection for .bmp, .pdf, .gif, Office document has been improved. - A endless loop bug and a memory leak have been fixed. - The I/O cache engine now caches read failure, it will speedup some recovery. @@ -428,7 +468,7 @@ New features: - can rebuild NTFS boot sector - can recover JFS partition - some advanced FAT32 functions have been added (Expert mode only) - + Improvements: - Can align partition to cylinder boundary or to head boundary. - Doesn't abort while writing partitions if read failed. diff --git a/thirdparty/photorec_exec/64-bit/bin/Qt5Core.dll b/thirdparty/photorec_exec/64-bit/bin/Qt5Core.dll new file mode 100644 index 0000000000..597b24afaa Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/Qt5Core.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/Qt5Gui.dll b/thirdparty/photorec_exec/64-bit/bin/Qt5Gui.dll new file mode 100644 index 0000000000..ede513c083 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/Qt5Gui.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/Qt5Widgets.dll b/thirdparty/photorec_exec/64-bit/bin/Qt5Widgets.dll new file mode 100644 index 0000000000..fe5f22e0a5 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/Qt5Widgets.dll differ diff --git a/thirdparty/photorec_exec/THANKS.txt b/thirdparty/photorec_exec/64-bit/bin/THANKS.txt similarity index 100% rename from thirdparty/photorec_exec/THANKS.txt rename to thirdparty/photorec_exec/64-bit/bin/THANKS.txt diff --git a/thirdparty/photorec_exec/64-bit/bin/VERSION.txt b/thirdparty/photorec_exec/64-bit/bin/VERSION.txt new file mode 100644 index 0000000000..6957aa4fb7 --- /dev/null +++ b/thirdparty/photorec_exec/64-bit/bin/VERSION.txt @@ -0,0 +1 @@ +7.1 Sun Jul 7 19:24:00 CEST 2019 diff --git a/thirdparty/photorec_exec/64-bit/bin/cygewf-2.dll b/thirdparty/photorec_exec/64-bit/bin/cygewf-2.dll new file mode 100644 index 0000000000..1f6485d7f2 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/cygewf-2.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/cyggcc_s-seh-1.dll b/thirdparty/photorec_exec/64-bit/bin/cyggcc_s-seh-1.dll new file mode 100644 index 0000000000..4872ea7937 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/cyggcc_s-seh-1.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/cygiconv-2.dll b/thirdparty/photorec_exec/64-bit/bin/cygiconv-2.dll new file mode 100644 index 0000000000..2c5e2d6ad1 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/cygiconv-2.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/cygjpeg-8.dll b/thirdparty/photorec_exec/64-bit/bin/cygjpeg-8.dll new file mode 100644 index 0000000000..a636bce75c Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/cygjpeg-8.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/cygncursesw-10.dll b/thirdparty/photorec_exec/64-bit/bin/cygncursesw-10.dll new file mode 100644 index 0000000000..f89ae1bf8f Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/cygncursesw-10.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/cygssp-0.dll b/thirdparty/photorec_exec/64-bit/bin/cygssp-0.dll new file mode 100644 index 0000000000..be465997a5 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/cygssp-0.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/cygwin1.dll b/thirdparty/photorec_exec/64-bit/bin/cygwin1.dll new file mode 100644 index 0000000000..4d0f3e4480 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/cygwin1.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/cygz.dll b/thirdparty/photorec_exec/64-bit/bin/cygz.dll new file mode 100644 index 0000000000..0f0b1c169b Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/cygz.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/documentation.html b/thirdparty/photorec_exec/64-bit/bin/documentation.html new file mode 100644 index 0000000000..1a73e99fa9 --- /dev/null +++ b/thirdparty/photorec_exec/64-bit/bin/documentation.html @@ -0,0 +1,16 @@ + + + +TestDisk & PhotoRec documentation + + + + +TestDisk & PhotoRec documentation can be found online: + + + diff --git a/thirdparty/photorec_exec/64-bit/bin/fidentify_win.exe b/thirdparty/photorec_exec/64-bit/bin/fidentify_win.exe new file mode 100644 index 0000000000..3cac0bd27b Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/fidentify_win.exe differ diff --git a/thirdparty/photorec_exec/64-bit/bin/iconv.dll b/thirdparty/photorec_exec/64-bit/bin/iconv.dll new file mode 100644 index 0000000000..31b2e564e9 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/iconv.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/libbz2-1.dll b/thirdparty/photorec_exec/64-bit/bin/libbz2-1.dll new file mode 100644 index 0000000000..f06d0da7da Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/libbz2-1.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/libewf-2.dll b/thirdparty/photorec_exec/64-bit/bin/libewf-2.dll new file mode 100644 index 0000000000..4fed04561c Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/libewf-2.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/libfreetype-6.dll b/thirdparty/photorec_exec/64-bit/bin/libfreetype-6.dll new file mode 100644 index 0000000000..6d28862147 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/libfreetype-6.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/libgcc_s_seh-1.dll b/thirdparty/photorec_exec/64-bit/bin/libgcc_s_seh-1.dll new file mode 100644 index 0000000000..83ac7ed525 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/libgcc_s_seh-1.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/libglib-2.0-0.dll b/thirdparty/photorec_exec/64-bit/bin/libglib-2.0-0.dll new file mode 100644 index 0000000000..98ecfd6299 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/libglib-2.0-0.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/libharfbuzz-0.dll b/thirdparty/photorec_exec/64-bit/bin/libharfbuzz-0.dll new file mode 100644 index 0000000000..ffa7341036 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/libharfbuzz-0.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/libintl-8.dll b/thirdparty/photorec_exec/64-bit/bin/libintl-8.dll new file mode 100644 index 0000000000..2e52e29f80 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/libintl-8.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/libjpeg-62.dll b/thirdparty/photorec_exec/64-bit/bin/libjpeg-62.dll new file mode 100644 index 0000000000..0dc4e40e49 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/libjpeg-62.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/libpcre-1.dll b/thirdparty/photorec_exec/64-bit/bin/libpcre-1.dll new file mode 100644 index 0000000000..bddc78b22e Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/libpcre-1.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/libpcre2-16-0.dll b/thirdparty/photorec_exec/64-bit/bin/libpcre2-16-0.dll new file mode 100644 index 0000000000..78b0b1d93a Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/libpcre2-16-0.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/libpng16-16.dll b/thirdparty/photorec_exec/64-bit/bin/libpng16-16.dll new file mode 100644 index 0000000000..c3f7b228ff Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/libpng16-16.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/libssp-0.dll b/thirdparty/photorec_exec/64-bit/bin/libssp-0.dll new file mode 100644 index 0000000000..8731e83ee7 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/libssp-0.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/libstdc++-6.dll b/thirdparty/photorec_exec/64-bit/bin/libstdc++-6.dll new file mode 100644 index 0000000000..ed82bbcf66 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/libstdc++-6.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/libwinpthread-1.dll b/thirdparty/photorec_exec/64-bit/bin/libwinpthread-1.dll new file mode 100644 index 0000000000..27a4b1a1a7 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/libwinpthread-1.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/photorec_win.exe b/thirdparty/photorec_exec/64-bit/bin/photorec_win.exe new file mode 100644 index 0000000000..8cd8e8a9ec Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/photorec_win.exe differ diff --git a/thirdparty/photorec_exec/64-bit/bin/platforms/qwindows.dll b/thirdparty/photorec_exec/64-bit/bin/platforms/qwindows.dll new file mode 100644 index 0000000000..d3ae040f6d Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/platforms/qwindows.dll differ diff --git a/thirdparty/photorec_exec/64-bit/bin/qphotorec_win.exe b/thirdparty/photorec_exec/64-bit/bin/qphotorec_win.exe new file mode 100644 index 0000000000..f4255aa1eb Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/qphotorec_win.exe differ diff --git a/thirdparty/photorec_exec/64-bit/bin/readme.txt b/thirdparty/photorec_exec/64-bit/bin/readme.txt new file mode 100644 index 0000000000..619b722eb8 --- /dev/null +++ b/thirdparty/photorec_exec/64-bit/bin/readme.txt @@ -0,0 +1,19 @@ +The Windows version of TestDisk & PhotoRec should work under the following 64 bits edition of +- Windows XP 64 +- Windows Server 2003 +- Windows Server 2008 +- Windows Server 2012 +- Windows Vista +- Windows 7 +- Windows 8 + +32-bits version can be downloaded from +https://www.cgsecurity.org/wiki/TestDisk_Download + +TestDisk doesn't need to be installed, you only need to +- extract the files +- run testdisk_win.exe or photorec_win.exe + +TestDisk & PhotoRec documentation can be found online: +- https://www.cgsecurity.org/wiki/TestDisk +- https://www.cgsecurity.org/wiki/PhotoRec diff --git a/thirdparty/photorec_exec/64-bit/bin/testdisk.pdf b/thirdparty/photorec_exec/64-bit/bin/testdisk.pdf new file mode 100644 index 0000000000..c8ccc59522 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/testdisk.pdf differ diff --git a/thirdparty/photorec_exec/64-bit/bin/testdisk_win.exe b/thirdparty/photorec_exec/64-bit/bin/testdisk_win.exe new file mode 100644 index 0000000000..e22107a783 Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/testdisk_win.exe differ diff --git a/thirdparty/photorec_exec/64-bit/bin/zlib1.dll b/thirdparty/photorec_exec/64-bit/bin/zlib1.dll new file mode 100644 index 0000000000..ec495aad0f Binary files /dev/null and b/thirdparty/photorec_exec/64-bit/bin/zlib1.dll differ diff --git a/thirdparty/photorec_exec/INFO b/thirdparty/photorec_exec/INFO deleted file mode 100644 index d913ceb8c2..0000000000 --- a/thirdparty/photorec_exec/INFO +++ /dev/null @@ -1,3 +0,0 @@ -TestDisk & PhotoRec , http://www.cgsecurity.org -Copyright (C) 1998-2015 Christophe GRENIER - diff --git a/thirdparty/photorec_exec/QtCore4.dll b/thirdparty/photorec_exec/QtCore4.dll deleted file mode 100755 index e411483e68..0000000000 Binary files a/thirdparty/photorec_exec/QtCore4.dll and /dev/null differ diff --git a/thirdparty/photorec_exec/QtGui4.dll b/thirdparty/photorec_exec/QtGui4.dll deleted file mode 100755 index 31e38d3467..0000000000 Binary files a/thirdparty/photorec_exec/QtGui4.dll and /dev/null differ diff --git a/thirdparty/photorec_exec/README.txt b/thirdparty/photorec_exec/README.txt new file mode 100644 index 0000000000..b65f25d7bd --- /dev/null +++ b/thirdparty/photorec_exec/README.txt @@ -0,0 +1,6 @@ +The 'bin' folder is the version used when running the PhotoRec ingest module. It is also the 32-bit version. +When the 64-bit version of the installer is created, the photorec_exec/64-bit/bin folder is placed at photorec_exec/bin. +When the 32-bit version of the installer is created, the photorec_exec/64-bit folder is deleted. +See 'build-windows-installer.xml' for more details. + +Extensions for PhotoRec need to be placed in the PhotoRecCarverFileOptExtensions class so that only valid extensions will be used with PhotoRec. It can be generated through PhotoRec by launching photorec_win with no arguments, go to "Proceed", go to "File Opt" and press 'b'. This should generate a photorec.cfg file in the current working directory with a list of all the extensions. \ No newline at end of file diff --git a/thirdparty/photorec_exec/VERSION.txt b/thirdparty/photorec_exec/VERSION.txt deleted file mode 100644 index f8aa4a2dc9..0000000000 --- a/thirdparty/photorec_exec/VERSION.txt +++ /dev/null @@ -1 +0,0 @@ -7.0 Sat Apr 18 13:02:01 CEST 2015 diff --git a/thirdparty/photorec_exec/bin/63/cygwin b/thirdparty/photorec_exec/bin/63/cygwin new file mode 100644 index 0000000000..56560d23a7 Binary files /dev/null and b/thirdparty/photorec_exec/bin/63/cygwin differ diff --git a/thirdparty/photorec_exec/bin/AUTHORS.txt b/thirdparty/photorec_exec/bin/AUTHORS.txt new file mode 100644 index 0000000000..fa1fc0ee7a --- /dev/null +++ b/thirdparty/photorec_exec/bin/AUTHORS.txt @@ -0,0 +1,4 @@ + +TestDisk and PhotoRec are written and maintained by Christophe GRENIER +TestDisk logos has been created by Simone Brandt and by Dmitri Zdorov in 2001, +PhotoRec logo by Marcel Bruins in 2006. diff --git a/thirdparty/photorec_exec/bin/COPYING.txt b/thirdparty/photorec_exec/bin/COPYING.txt new file mode 100644 index 0000000000..d511905c16 --- /dev/null +++ b/thirdparty/photorec_exec/bin/COPYING.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/thirdparty/photorec_exec/bin/INFO b/thirdparty/photorec_exec/bin/INFO new file mode 100644 index 0000000000..da71e56064 --- /dev/null +++ b/thirdparty/photorec_exec/bin/INFO @@ -0,0 +1,2 @@ +TestDisk & PhotoRec , https://www.cgsecurity.org +Copyright (C) 1998-2019 Christophe GRENIER diff --git a/thirdparty/photorec_exec/bin/NEWS.txt b/thirdparty/photorec_exec/bin/NEWS.txt new file mode 100644 index 0000000000..263557d826 --- /dev/null +++ b/thirdparty/photorec_exec/bin/NEWS.txt @@ -0,0 +1,476 @@ +Current news +== 7.1 == +== General Improvements == +* It should be possible to reproduce, byte for byte, every build of testdisk package in Debian https://wiki.debian.org/ReproducibleBuilds +* Windows: Identify again the device model (Regression present in 7.0 version) + +== TestDisk == +=== Improvements === +* Ask confirmation when user quits the list of partitions found by TestDisk +=== Bug fixes === +* Various fix for Scripted run + +== PhotoRec & QPhotoRec == +=== Improvements === +* Log the correct filenames in photorec.log file +* Reduced false positives for tar +* Fix recovery of mobius camera videos. +* Fix recovery of progressive jpeg +* Extract the filesize from sqlite header when available +* Recover more fragmented files + +Extract of the new file formats recovered by PhotoRec: +* .aes: Synology AES key files +* .axp: Pinnacle Studio +* .bdm: AVHCD index +* .cpi: AVCHD Clip Information +* .DS_Store: Apple Desktop Services Store +* .dvr: RT60 +* .gi: Roxio Creator +* .gpx: Guitar Pro 6 +* .gp4: Guitar Pro 4 +* .iam/.ipt Autodesk Inventor part +* .icns: Apple Icon Image +* .jsonlz4: Mozilla bookmarks +* .lyx LyX 2.X +* .mpl: AVHCD playlist +* .tg Tux Guitar 1.2 +* .vdj: VirtualDJ +* .wee: weecast +* .xar: XAR Archive + +== 7.0 == + +=== General Improvements === +Various fix including security fix, thanks to +* Coverity scan (Static Analysis of source code) +* afl-fuzz (security-oriented fuzzer). +* Denis Andzakovic from Security Assessment for reporting an exploitable Stack Buffer Overflow + +=== TestDisk === +==== Improvements ==== +* exFAT: better support +* ext4: handle 64 bit blocks or 64 KiB blocksize. Fix detection and file listing + +==== Bug fixes ==== +* Avoid erroneous error when writing 512 bytes on hard disk using 4k sector +* FAT, NTFS: avoid NULL pointer dereference if localtime() returns NULL. Thanks to Graham Sutherland for reporting this bug. + +=== PhotoRec & QPhotoRec === +QPhotoRec is a Graphical User Interface (Qt based GUI) version of PhotoRec. More user friendly, it recognizes the same file formats. + +PhotoRec remains recommended for advanced users, it can stop a recovery and resume it later, it recovers more fragmented files when brute-force technology is enabled and expert mode is available. + +==== Improvements ==== +* Reduced false positives for more than 80 file formats. +* .gif: fix filesize detection +* .flv: add Flash filesize detection +* .mpg: detect filesize for MPEG +* .ra: detect filesize for RealAudio3 +* Improved algorithm to deal with data fragmentation resulting in a general speed increased +* Speedup brute-force mode. Brute-force mode can recover more fragmented files, but it's still slow and not 100% reliable. You can enable it in PhotoRec Options menu. + +New file formats recovered by PhotoRec: +* .3dm: Rhino / openNURBS +* .ari: ARRI Raw Video +* .camrec: Camtasia Studio +* .dad: Micae DVR +* .dcm: Digital Imaging and Communications in Medicine (DICOM) +* .fp12: File Maker Pro 12 +* .kra: Krita +* .mlv: Magic Lantern Video +* .notebook: SMART notebook +* .ora: Mypaint +* .red: RED2 video format +* .rlv: Revelation password +* .vbm: Veeam Backup Metadata +* .woff: Web Open Font Format + + + +== 6.14 == +* The log file generated by the Windows version (cygwin) reports bad sectors in a more readable fashion, example +ReadFile Data error (cyclic redundancy check). +* As openssl isn't used, don't link with this cryptographic library (Debian tries to avoid mixing GPL code and openssl) + +=== TestDisk === +==== Improvements ==== +* testdisk /list now displays the disk model, serial number, firmware version and hpa or dco presence if detected +* Recover WBFS (Wii Backup File System) partition +* Make FAT RebuildBS works when there is a single FAT table +* Interface: Display the partition table type if autodetected +* Interface: modified warning about mismatching geometry between FAT or NTFS boot sector and HD geometry information ([http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=651756 Debian #651756]) +* Interface: Remove "Allow partial last cylinder" option + +==== Bug fixes ==== +* Fix crc in EFI backup GPT +* Rewrote how TestDisk aligns partition on cylinder or 1MB boundary. It avoids to create partition entry where the partition ends after the end of the disk. + +=== PhotoRec === +==== Improvements ==== +* Improve Olympus .orf recovery +* Improve WP Mac/WP5/WP6 Corel Documents .wpd files recovery +* Fix thumbs.db recovery, avoid some false positive with .doc +* Interface: if less than 10 file families are enabled, display the results even if zero has been found yet + +New file formats: +* .aep After Effects +* .axx AxCrypt +* .dp Designer, a Photobook Designer Software +* .lzh archive +* .mmap MindManager +* .plt Gerber Graphix Advantage +* .prproj Adobe Premiere project +* .psb Adobe Photoshop Image +* .pts PTGui, panoramic stitching software +* .qcp The QCP File Format and Media Types for Speech Data (RFC3625) +* .shn Shorten audio file +* .snt Windows Sticky Notes +* .ttd TinyTag Data +* .wallet Armory bitcoin wallet +* .wim Windows imaging (WIM) image + +==== Bug fixes ==== +* Fix an endless loop during .caf file recovery +* Fix tiff recovery including some raw file formats, 64-bit version wasn't affected + + +== 6.13 == +Fix UAC manifests for Windows, so users don't need to use right-click "Run As Administrator" + +TestDisk +- Fix image creation, image.dd file wasn't created (Regression introduced in 6.12) +- Detect Vmware VMFS partition +- Locate lost GFS2 partition but not yet the size +- Log HDD serial number and firmware revision +- List NTFS Alternate Data Streams (ADS) + +PhotoRec +- Session recovery restarts at the previous location +- Better MPEG recovery, there should be less concatenated videos. +- Better JPG recovery, there should be less cases where thumbnails were recovered instead of the picture itself. +- Handle large avi files using "AVIX" or mov files using 64-bit chunk size. +- Rename recovered pdf using the title (not perfect) +- Major cleanup of PhotoRec core code + +6.12 +fidentify, a little utility sharing PhotoRec signature database, is now build by default. It identifies the type of data contained in a file and reports the extension as seen by PhotoRec. It is similar to the Unix file command. Add compatibility with +- libewf 20110312 +- ncurses 5.8 +- ntfs-3g +Fix detection of Encase 6 .ewf files + +TestDisk +- Convert the directory name when it can't be created (Fix for Windows/Cygwin version) +- Better HPA/DCO detection: handle the case where native_max is null. +- Image Creation is now faster than previous version when there are bad sectors +- List and copy (experimental) files from exFAT filesystem +- Improved NTFS undelete interface + +PhotoRec +- PhotoRec checks for EFBIG (file too large) error when writing files. It's usefull to avoid erronous message about "no free space available" when recovering to a FAT filesystem. +- Recover files from exFAT unallocated space +- Use doc/xls/ppt title to name recovered Office document, use first filename in zip archive... +- Possibility to add your own extension/custom signature to PhotoRec +- Generated a report.xml file using Digital Forensics XML + +6.11 +TestDisk & PhotoRec 6.11 should use less CPU. +This new TestDisk version can undelete files for NTFS filesystem and recover deleted exFAT and ext4. +PhotoRec +- Performance improvement when scanning for numerous file type +- Fix several bugs including an endless loop, several memory leaks and several out-of-bound memory access +- 50 file formats have been added + +6.10 +TestDisk & PhotoRec 6.10 comes with severals improvements: +- Report disk manufacturer and model under Windows and Linux (Only Linux was + supported in 6.9) +- Under Linux, /dev/mapper/* and /dev/md? are now listed with the harddisks. +- Now both OS and compiler versions are recorded in the log file. + +This new TestDisk version can +- undelete files and directories for FAT filesystem, +- undelete files for ext2 filesystem, +- copy files from ext2/ext3 partitions. These feature was already available + for FAT and NTFS. + +PhotoRec +- Load and save FileOpts settings: remember which file types to recover +- For JPEG files, extract the time/date from Exif header and set the + file time. It's now easier to sort the recovered jpg. +- PhotoRec can now identify and recover 38 additional file formats. + +6.9 +TestDisk & PhotoRec 6.9 comes with numerous improvements: +- They are compatible with ntfsprogs 2.0, the latest library version for +accessing NTFS partitions, e2fsprogs 1.40.6, the latest library for accessing + ext2/ext3 partitions. +- EFI GUID Partition Table is now supported. EFI GPT is mainly used on Itanium, +MacBook and Mac Pro. +- Both utilities can use sudo if the user is not root, this functionnality will +be enabled for at least MacOSX, so users won't have to go into a command line. +- Improved Windows disk support, most internal USB card reader should now work. +- Disk model (ie. ATA ST3120026AS, _NEC DVD_RW ND-4550A...) are now reported +under Linux. + +TestDisk +- New file system support has been added: encrypted LUKS, Mac HFSX, Linux Raid +md 1.0/1.1/1.2 (0.9 was already supported). +- It displays unicode filenames correctly, and can handle unicode filesnames + while copying files from an NTFS partition when supported by the underlying + libraries. +- It's now possible to copy files from a lost FAT partition found by TestDisk + (Already possible for NTFS). +- In the Advanced menu, TestDisk can create a raw/dd-like image of a partition. + +PhotoRec +- For ext2/ext3 file systems, PhotoRec 6.9 can search in the whole space or in +unallocated space only. This feature was already available for FAT and NTFS +filesystem. +- It has better session support which allows a recovery to be stopped and +restarted later. +- New file formats are supported: Acronis True Image .TIB, AutoCAD's .DWG and +PowerTab .PTB, Cineon image file/SMTPE DPX .DPX, Comic Life .comicdoc, HP +Photosmart Photo Printing Album .albm, KeepAssX .KDB, Maya .MB and .MP, +Microsoft OneNOte .one file, Microsoft Visual Studio Resource file .RES, +Microsoft VB's .CLS, Outlook .MSG, QuickBook .FST, SketchUp .SKP, Vmware .vmdk, +WinSpec .SPE, Windows Enhanced MetaFile .emf, MS Windows Link .lnk, Internet +Explorer index.dat, Macintosh Picture .pct, SunPCI Disk Image,XBOX GTA San +Andreas Save File, Final Cut Pro .fcp and Digital Speech Standard .dss. + +6.8 +Version 6.8 is mainly a bugfix but some small improvements has been made to +both TestDisk and PhotoRec: +- Partition type is now autodetected. +- TestDisk and PhotoRec can now be used under screen, the screen manager with +VT100/ANSI terminal emulation. + +TestDisk +HFS detection has been improved to avoid false positive. +TestDisk logs potential NTFS partition location from MFT & MFTMirr location +while rebuilding NTFS boot sector. + +PhotoRec +Some important bugfixes (false positive problem, implementation bugs) have been +made in PhotoRec 6.8. JPG bruteforce recovery has been improved a little bit, +you can use it to recover more fragmented jpeg but it's very time consuming and +not 100% reliable, so it's still disabled by default. +.7z, .cab, .rar and .tar recovery have been improved, .dta and .spss file +formats have been added. + +6.7 +Both utilities are faster than previous versions: a better disk caching is +used in PhotoRec, a better way to handle the sector to scan is used in +TestDisk. Windows version of TestDisk and PhotoRec doesn't use +pread()/pwrite() cygwin implementation (A bug seems present). + +TestDisk +TestDisk 6.7 handles partition created under Microsoft Vista. +Traditionally partitions were created on cylinder boundaries. Under Vista, +they are now independent of the disk geometry: partitions are aligned to 1 +MB boundary, TestDisk now handles that. It also fixes how the advanced +menu works. + +PhotoRec +PhotoRec 6.7 uses less CPU. It also adds support for 3ds max, Archive +.ace, CD Audio .cda, FastTrackerII Extended Module .xm, Linux archive .a, +Linux/Unix ELF binary, Mac OS .emlx mail format, Macomedia Compressed +Flash .swc, Macromedia .flv, Macromedia Freehand 5 (.fh5) & 10 (.fh10) and +InDesign .indd, Matroska .mkv, MP3 with ID3 header, MS cabinet archive +.cab, MSOffice "Open" XML .docx, .pptx, .xlsx, MS executable (PE), MS +Windows Metafile .wmf, NJStar .njx, Quickbooks .qbb and .qbw, Real Audio +.rm, registry config file .reg, RPM package .rpm, Windows registry header +detection and Event Log .evt + +6.6 +General Improvements +- Encase Expert Witness Compression Format is now supported, so Computer +Forensic Experts can use TestDisk and PhotoRec more easily. +- Under new Vista OS, harddisks are now being reported again. +TestDisk +- If LBA48 support seems missing (HD bigger than 130 GB not supported by +the OS), TestDisk will warn the user. This problem is common when a user +reinstalls his OS and forgets to install the latest service patches. +- NTFS: TestDisk should be able to list files from NTFS partitions even if +some filesystem corruption has occurred; more importantly, it will allow +the user to copy whatever file data it can as well. +- FAT: Improved heuristics to find the first FAT area during boot sector +rebuild. +PhotoRec +- A new method for handling fragmented data is now used, making recovery +more reliable and faster. +- PhotoRec can be set to search for files in FAT16/FAT32 unallocated space +only; which avoids wasting time recovering files that are still +accessible, making the recovery of 'lost files' much faster and more +efficient. +- New file formats have been added: .3g2 (Mov video family), .aif Apple +Audio, .all .cpr Cubase Song, .blend Blender, .cam Casio QV Digital Camera +Image, .flac Free Lossless Audio Codec, .mdf Microsoft SQL, .swf +Macromedia Flash and .vcf VCard (not confused with .txt anymore) + +6.5 +TestDisk +- A screen has been added to control the log file creation. +- It's now possible from TestDisk file listing to copy files from NTFS +partition to a selected directory. +- NTFS MFT can also be repaired in more cases. +- A compilation problem has been fixed with old version of libntfs. +- Documentation has been fixed, it now displays correctly with IE. +PhotoRec +- New file formats have been added: AppleWorks .cwk, DIF Digital Video .dv, +DjVu .djv, Finale .mus, Incredimail .imm, .imb, iTunes mhbd, MIDI .mid, +MS Backup, Real Media .rm & .ram, Reason .rns, ruby .rb, .xml +- File size detection for .bmp, .pdf, .gif, Office document has been improved. +- A endless loop bug and a memory leak have been fixed. +- The I/O cache engine now caches read failure, it will speedup some recovery. + +6.4 +Support for ReiserFS 4 has been added in TestDisk +PhotoRec detects new files formats and better recovery of jpeg, txt/html, zip... +Fixes numerous bugs. + +6.3 +Fix for 64 bits architecture and improved interface. +Photorec adds support for Quicken, OpenDocument, mp3, ogg, +StarOffice, Encapsulated PostScript, PostScript, gif, gz, mov file format +or better detection. + +6.2 +New User Interface +Disk cache and read ahead to improve performance +Endianess portability fix + +6.1 +TestDisk +Fix for Mac version +Remove an erroneous message after writing a partition without logical partition + +6.0 +TestDisk +Fix for Dos version and some cosmetic change + +5.9 +TestDisk +- User Interface has been improved +- Mac partition map is now supported +- FAT: better check for directory attribut +- FAT: fix a bug in expert mode +- FAT: fix directory listing time using timezone +- Linux Raid: fix regression from 5.8, Raid 1 is again detected. +- NTFS boot sector: copy boot sector over backup boot sector now works +Photorec +- Photorec now works on ext2/ext3 filesystem (Check Options) +- add support for Papyrus, 7zip, text file, OGG audio files, RAR archive recovery +- add a default file size limit of 2GB +- add support for ZIP files starting with PK00 (packed to removable disk) + +5.8 +TestDisk +- Add support for HFS+, UFS2 filesystem and Intel Solaris superblock +- Linux Raid: Add some code to detect Raid 5 earlier +- FAT: Update the cleaning FAT function to repair FAT table (Expert mode only) +- EXT2: e2fsprogs-1.36 parses the device name given in ext2fs_open, give a string instead of the io_channel. +- NTFS: Fix a bug in NTFS rebuilding introduced in 5.7 +- Fix a memory freed problem when detecting if a partition can be Primary, Logical... +Photorec +- Recognize php header +- MPG format is using streaming + +5.7 +TestDisk +- Replace standard MBR i386 boot sector code by a GPL one +- HFS detection has been improved to avoid false positive +- FAT: add support for one FAT only (instead of usual 2) in directory listing + and boot sector rebuilding (need expert mode) +- FAT: Support for FAT without FAT12 (ie DOS 3.30), FAT16 or FAT32 mark. +- FAT12/16: Add the possibility to initialize FAT root directory (Delete everything, + Expert mode only) +- FAT: fix for directory listing in boot sector rebuilding. +- FAT32: Fix for last FAT sector while cleaning the FAT (Expert mode only) +- EXT2/EXT3: while listing filename permit to list file where inode information is unavaible. +- Doesn't halt if TestDisk can't create the log file +- configure.ac, compile.sh: remove --enable-debug option +- Modify Linux RPM spec file to get non-empty debug-info rpm +- Directory listing: little UI modification +- Dos version: fix read/write error message +Photorec 5.7 +- add support for a bunch of other file format +- incremental directory name for recovery +- a lot of code cleaning + +5.6 +TestDisk +New features: +- Can list files from NTFS partition found using backup boot sector +- Display a warning if TestDisk think the logical geometry (CHS) is wrong +- Handle filesystem image (In Options, Partition type: None) +- Filesystem: HFS (minimal testing) and LVM2 support +- NetBSD and FreeBSD support + +Improvements: +- Win32: Windows version is now as fast as other version. + Using FileRead() instead of read() is about 10 times faster. + +Fixs: +- HD Geometry: the number of cylinders can be bigger than 65535 +- BSD slice: display content even if crc is wrong +- FAT boot sector rebuilding: better support for FAT16 converted to FAT32 +- NTFS boot sector rebuilding: better handle copy of MFT +- Win32: Windows version always creates a log file (was introduced in 5.3 but + boggus since 5.4) + +Photorec 5.6 +- Use libjpeg for a better recovery of lost jpeg files. +- Konica/Minolta raw (MRW) pictures recovery +- Canon raw (CRW) pictures recovery +- Minimal support for Sigma/Foveon: .X3F, Rollei (RDC), Fuji (RAF) + +5.5 +Fix FAT32 recovery using backup boot sector +Remove some debugging code +Upgrade to libntfs 1.9.4 +Fix RPM spec file to get a working reiserfs support + +5.4 +New features: +- Option to backup unknown partition header. +- Save/Load current partition list +- handle sector size != 512 +- XFS and CramFS support + +Improvements: +- Interface has been improved. +- log libreiserfs errors in the log file +- can choose to minimize or maximize the extended partition before writing +- FAT32 root cluster rebuild improved (use first free cluster, mark it as used or/and EOC) + +Fixs: +- ReiserFS and EXT2/EXT3 directory listing fix +- fix a bug with BeFS partition recovery + +5.3 +New features: +- When rebuilding FAT boot sector in expert mode, lets the user choose FAT locations. + +Improvements: +- Exit if TestDisk can't create log file. +- Windows version always create log file. + +Bug corrections: +- Fix superblock number displayed when telling the user to use e2fsck. +- Fix extended partition creation. + + +5.2 +TestDisk now compiles and run under NT 4 and Windows 2000. +New features: +- can rebuild NTFS boot sector +- can recover JFS partition +- some advanced FAT32 functions have been added (Expert mode only) + +Improvements: +- Can align partition to cylinder boundary or to head boundary. +- Doesn't abort while writing partitions if read failed. +- Doesn't let the user write an empty partition table. + diff --git a/thirdparty/photorec_exec/bin/Qt5Core.dll b/thirdparty/photorec_exec/bin/Qt5Core.dll new file mode 100644 index 0000000000..c0def22aea Binary files /dev/null and b/thirdparty/photorec_exec/bin/Qt5Core.dll differ diff --git a/thirdparty/photorec_exec/bin/Qt5Gui.dll b/thirdparty/photorec_exec/bin/Qt5Gui.dll new file mode 100644 index 0000000000..e2ebed7965 Binary files /dev/null and b/thirdparty/photorec_exec/bin/Qt5Gui.dll differ diff --git a/thirdparty/photorec_exec/bin/Qt5Widgets.dll b/thirdparty/photorec_exec/bin/Qt5Widgets.dll new file mode 100644 index 0000000000..2cdec6c4ae Binary files /dev/null and b/thirdparty/photorec_exec/bin/Qt5Widgets.dll differ diff --git a/thirdparty/photorec_exec/bin/THANKS.txt b/thirdparty/photorec_exec/bin/THANKS.txt new file mode 100644 index 0000000000..3dda34e63d --- /dev/null +++ b/thirdparty/photorec_exec/bin/THANKS.txt @@ -0,0 +1,6 @@ +TestDisk & PhotoRec are mainly written by Christophe GRENIER. +Many people further contributed to TestDisk, directly or indirectly, by +reporting problems, helping with the documentation, suggesting various +improvements, sending me gifts using my Amazon whish-list... + +Thanks to the thousands of people who have provided support for the project! diff --git a/thirdparty/photorec_exec/bin/VERSION.txt b/thirdparty/photorec_exec/bin/VERSION.txt new file mode 100644 index 0000000000..e525c812bc --- /dev/null +++ b/thirdparty/photorec_exec/bin/VERSION.txt @@ -0,0 +1 @@ +7.1 Sun Jul 7 19:23:25 CEST 2019 diff --git a/thirdparty/photorec_exec/bin/cygewf-2.dll b/thirdparty/photorec_exec/bin/cygewf-2.dll new file mode 100644 index 0000000000..dc0aaafe58 Binary files /dev/null and b/thirdparty/photorec_exec/bin/cygewf-2.dll differ diff --git a/thirdparty/photorec_exec/bin/cyggcc_s-1.dll b/thirdparty/photorec_exec/bin/cyggcc_s-1.dll new file mode 100644 index 0000000000..4f54327feb Binary files /dev/null and b/thirdparty/photorec_exec/bin/cyggcc_s-1.dll differ diff --git a/thirdparty/photorec_exec/cygiconv-2.dll b/thirdparty/photorec_exec/bin/cygiconv-2.dll old mode 100755 new mode 100644 similarity index 65% rename from thirdparty/photorec_exec/cygiconv-2.dll rename to thirdparty/photorec_exec/bin/cygiconv-2.dll index fe023d5156..0511362375 Binary files a/thirdparty/photorec_exec/cygiconv-2.dll and b/thirdparty/photorec_exec/bin/cygiconv-2.dll differ diff --git a/thirdparty/photorec_exec/bin/cygjpeg-8.dll b/thirdparty/photorec_exec/bin/cygjpeg-8.dll new file mode 100644 index 0000000000..986d408a7e Binary files /dev/null and b/thirdparty/photorec_exec/bin/cygjpeg-8.dll differ diff --git a/thirdparty/photorec_exec/bin/cygncursesw-10.dll b/thirdparty/photorec_exec/bin/cygncursesw-10.dll new file mode 100644 index 0000000000..e420ca3540 Binary files /dev/null and b/thirdparty/photorec_exec/bin/cygncursesw-10.dll differ diff --git a/thirdparty/photorec_exec/bin/cygssp-0.dll b/thirdparty/photorec_exec/bin/cygssp-0.dll new file mode 100644 index 0000000000..a92821f8f7 Binary files /dev/null and b/thirdparty/photorec_exec/bin/cygssp-0.dll differ diff --git a/thirdparty/photorec_exec/bin/cygwin1.dll b/thirdparty/photorec_exec/bin/cygwin1.dll new file mode 100644 index 0000000000..dd1510f848 Binary files /dev/null and b/thirdparty/photorec_exec/bin/cygwin1.dll differ diff --git a/thirdparty/photorec_exec/bin/cygz.dll b/thirdparty/photorec_exec/bin/cygz.dll new file mode 100644 index 0000000000..5c56923d0b Binary files /dev/null and b/thirdparty/photorec_exec/bin/cygz.dll differ diff --git a/thirdparty/photorec_exec/bin/documentation.html b/thirdparty/photorec_exec/bin/documentation.html new file mode 100644 index 0000000000..1a73e99fa9 --- /dev/null +++ b/thirdparty/photorec_exec/bin/documentation.html @@ -0,0 +1,16 @@ + + + +TestDisk & PhotoRec documentation + + + + +TestDisk & PhotoRec documentation can be found online: + + + diff --git a/thirdparty/photorec_exec/bin/fidentify_win.exe b/thirdparty/photorec_exec/bin/fidentify_win.exe new file mode 100644 index 0000000000..74047f0ebf Binary files /dev/null and b/thirdparty/photorec_exec/bin/fidentify_win.exe differ diff --git a/thirdparty/photorec_exec/bin/iconv.dll b/thirdparty/photorec_exec/bin/iconv.dll new file mode 100644 index 0000000000..6453cb2e71 Binary files /dev/null and b/thirdparty/photorec_exec/bin/iconv.dll differ diff --git a/thirdparty/photorec_exec/bin/libbz2-1.dll b/thirdparty/photorec_exec/bin/libbz2-1.dll new file mode 100644 index 0000000000..71bec753d8 Binary files /dev/null and b/thirdparty/photorec_exec/bin/libbz2-1.dll differ diff --git a/thirdparty/photorec_exec/bin/libewf-2.dll b/thirdparty/photorec_exec/bin/libewf-2.dll new file mode 100644 index 0000000000..013bc2f4d1 Binary files /dev/null and b/thirdparty/photorec_exec/bin/libewf-2.dll differ diff --git a/thirdparty/photorec_exec/bin/libfreetype-6.dll b/thirdparty/photorec_exec/bin/libfreetype-6.dll new file mode 100644 index 0000000000..f854b456d7 Binary files /dev/null and b/thirdparty/photorec_exec/bin/libfreetype-6.dll differ diff --git a/thirdparty/photorec_exec/bin/libgcc_s_sjlj-1.dll b/thirdparty/photorec_exec/bin/libgcc_s_sjlj-1.dll new file mode 100644 index 0000000000..1cca9415b0 Binary files /dev/null and b/thirdparty/photorec_exec/bin/libgcc_s_sjlj-1.dll differ diff --git a/thirdparty/photorec_exec/bin/libglib-2.0-0.dll b/thirdparty/photorec_exec/bin/libglib-2.0-0.dll new file mode 100644 index 0000000000..def5261596 Binary files /dev/null and b/thirdparty/photorec_exec/bin/libglib-2.0-0.dll differ diff --git a/thirdparty/photorec_exec/bin/libharfbuzz-0.dll b/thirdparty/photorec_exec/bin/libharfbuzz-0.dll new file mode 100644 index 0000000000..b026938b12 Binary files /dev/null and b/thirdparty/photorec_exec/bin/libharfbuzz-0.dll differ diff --git a/thirdparty/photorec_exec/bin/libintl-8.dll b/thirdparty/photorec_exec/bin/libintl-8.dll new file mode 100644 index 0000000000..f2f275850b Binary files /dev/null and b/thirdparty/photorec_exec/bin/libintl-8.dll differ diff --git a/thirdparty/photorec_exec/bin/libjpeg-62.dll b/thirdparty/photorec_exec/bin/libjpeg-62.dll new file mode 100644 index 0000000000..e4932a5cf5 Binary files /dev/null and b/thirdparty/photorec_exec/bin/libjpeg-62.dll differ diff --git a/thirdparty/photorec_exec/bin/libpcre-1.dll b/thirdparty/photorec_exec/bin/libpcre-1.dll new file mode 100644 index 0000000000..5c884cc38f Binary files /dev/null and b/thirdparty/photorec_exec/bin/libpcre-1.dll differ diff --git a/thirdparty/photorec_exec/bin/libpcre2-16-0.dll b/thirdparty/photorec_exec/bin/libpcre2-16-0.dll new file mode 100644 index 0000000000..f458a695ca Binary files /dev/null and b/thirdparty/photorec_exec/bin/libpcre2-16-0.dll differ diff --git a/thirdparty/photorec_exec/bin/libpng16-16.dll b/thirdparty/photorec_exec/bin/libpng16-16.dll new file mode 100644 index 0000000000..85b6110291 Binary files /dev/null and b/thirdparty/photorec_exec/bin/libpng16-16.dll differ diff --git a/thirdparty/photorec_exec/bin/libssp-0.dll b/thirdparty/photorec_exec/bin/libssp-0.dll new file mode 100644 index 0000000000..4d27e3dbcf Binary files /dev/null and b/thirdparty/photorec_exec/bin/libssp-0.dll differ diff --git a/thirdparty/photorec_exec/bin/libstdc++-6.dll b/thirdparty/photorec_exec/bin/libstdc++-6.dll new file mode 100644 index 0000000000..2a28df6d22 Binary files /dev/null and b/thirdparty/photorec_exec/bin/libstdc++-6.dll differ diff --git a/thirdparty/photorec_exec/bin/libwinpthread-1.dll b/thirdparty/photorec_exec/bin/libwinpthread-1.dll new file mode 100644 index 0000000000..73399cdb32 Binary files /dev/null and b/thirdparty/photorec_exec/bin/libwinpthread-1.dll differ diff --git a/thirdparty/photorec_exec/bin/photorec_win.exe b/thirdparty/photorec_exec/bin/photorec_win.exe new file mode 100644 index 0000000000..90fdbd71e2 Binary files /dev/null and b/thirdparty/photorec_exec/bin/photorec_win.exe differ diff --git a/thirdparty/photorec_exec/bin/platforms/qwindows.dll b/thirdparty/photorec_exec/bin/platforms/qwindows.dll new file mode 100644 index 0000000000..b65c706a6c Binary files /dev/null and b/thirdparty/photorec_exec/bin/platforms/qwindows.dll differ diff --git a/thirdparty/photorec_exec/plugins/BartPE/Get_Files.cmd b/thirdparty/photorec_exec/bin/plugins/BartPE/Get_Files.cmd old mode 100755 new mode 100644 similarity index 81% rename from thirdparty/photorec_exec/plugins/BartPE/Get_Files.cmd rename to thirdparty/photorec_exec/bin/plugins/BartPE/Get_Files.cmd index 9a5187097e..80ba9d2383 --- a/thirdparty/photorec_exec/plugins/BartPE/Get_Files.cmd +++ b/thirdparty/photorec_exec/bin/plugins/BartPE/Get_Files.cmd @@ -13,7 +13,7 @@ set INFname=Testdisk.inf ::SET NUMROW= 0%% 25%% 50%% 75%% 100%% echo ===================================== > "%cd%\File_Grabber.log" -echo Testdisk and PhotoRec 6.14 Plugin >> File_Grabber.log +echo Testdisk and PhotoRec 7.1 Plugin >> File_Grabber.log echo Plugin by: Xtreme (Ahmed Hossam) >> File_Grabber.log echo Collector by: Xtreme >> File_Grabber.log echo Plugin for: Windows Xpire Rd CD >> File_Grabber.log @@ -32,7 +32,25 @@ IF NOT EXIST "%cd%\TestDisk_PE\files\63" md "TestDisk_PE\files\63" echo.&echo -Grabbing file: Copy Testdisk files... &FOR %%A IN ( ..\..\cygwin1.dll +..\..\cyggcc_s-1.dll +..\..\cygiconv-2.dll +..\..\cygjpeg-8.dll +..\..\cygncursesw-10.dll +..\..\cygssp-0.dll +..\..\cygwin1.dll +..\..\cygz.dll +..\..\iconv.dll +..\..\libgcc_s_sjlj-1.dll +..\..\libjpeg-62.dll +..\..\libpng16-16.dll +..\..\libssp-0.dll +..\..\libstdc++-6.dll +..\..\libwinpthread-1.dll +..\..\QtCore4.dll +..\..\QtGui4.dll +..\..\zlib1.dll ..\..\photorec_win.exe +..\..\qphotorec_win.exe ..\..\testdisk_win.exe ..\..\fidentify_win.exe )DO (ECHO. Grabbing file:%%~A >>"%cd%\File_Grabber.log" @@ -70,20 +88,20 @@ GOTO :EOF MODE CON: COLS=78 LINES=28 &COLOR 4F &cls echo.&echo. >> File_Grabber.log -echo TestDisk and PhotoRes can't be localized on your system >> File_Grabber.log +echo TestDisk and PhotoRec can't be localized on your system >> File_Grabber.log echo. >> File_Grabber.log -echo You can download TestDisk and PhotoRes from here >> File_Grabber.log +echo You can download TestDisk and PhotoRec from here >> File_Grabber.log echo.&echo -TestDisk Official website: >> File_Grabber.log -echo http://www.cgsecurity.org/wiki/TestDisk_Download/ >> File_Grabber.log +echo https://www.cgsecurity.org/wiki/TestDisk_Download/ >> File_Grabber.log echo. >> File_Grabber.log echo For help you can check Help.html >> File_Grabber.log CALL :BRANDH -echo.&echo. TestDisk and PhotoRes can't be localized on your system +echo.&echo. TestDisk and PhotoRec can't be localized on your system echo.&echo. You need to download TestDisk and DON'T change the download folders structure then try again -echo.&echo. You can download TestDisk and PhotoRes from here +echo.&echo. You can download TestDisk and PhotoRec from here echo.&echo. -TestDisk Official website: -echo http://www.cgsecurity.org/wiki/TestDisk_Download/&echo. +echo https://www.cgsecurity.org/wiki/TestDisk_Download/&echo. CALL :BRAND2 GOTO :END @@ -101,7 +119,7 @@ exit :done cls&CALL :BRANDH -echo.&echo. TestDisk and PhotoRes Files have been succesfully collected +echo.&echo. TestDisk and PhotoRec Files have been succesfully collected echo.&echo. You can now start build your PE version &echo. CALL :BRAND2 GOTO :END @@ -111,7 +129,7 @@ MODE CON: COLS=75 LINES=35 &color 1e echo. ECHO ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ» ECHO º º -ECHO º TestDisk and PhotoRes plugin Files Collector º +ECHO º TestDisk and PhotoRec plugin Files Collector º ECHO º º ECHO º Plugin by Xtreme ( Xtremesony_xp@yahoo.com ) º ECHO º º diff --git a/thirdparty/photorec_exec/plugins/BartPE/Help.htm b/thirdparty/photorec_exec/bin/plugins/BartPE/Help.htm similarity index 93% rename from thirdparty/photorec_exec/plugins/BartPE/Help.htm rename to thirdparty/photorec_exec/bin/plugins/BartPE/Help.htm index a4e7324c9a..3bf718d981 100644 --- a/thirdparty/photorec_exec/plugins/BartPE/Help.htm +++ b/thirdparty/photorec_exec/bin/plugins/BartPE/Help.htm @@ -30,7 +30,7 @@

Instructions

    -
  • 1- Download TestDisk and PhotoRec from Here
  • +
  • 1- Download TestDisk and PhotoRec from Here
  • 2- Uncompress the download.
  • 3- Run the Get_Files.cmd to collect the needed files.
  • 4- Copy this folder to Plugin directory.
  • @@ -41,7 +41,7 @@
  • Forum Plugin Support Here
  • More Plugins Here (mirror).
  • You can mail Xtreme for any help about the plugin.
  • -
  • For help about TestDisk and PhotoRec, contact CG Security support.
  • +
  • For help about TestDisk and PhotoRec, contact CGSecurity forum.
diff --git a/thirdparty/photorec_exec/plugins/BartPE/RESET.cmd b/thirdparty/photorec_exec/bin/plugins/BartPE/RESET.cmd old mode 100755 new mode 100644 similarity index 100% rename from thirdparty/photorec_exec/plugins/BartPE/RESET.cmd rename to thirdparty/photorec_exec/bin/plugins/BartPE/RESET.cmd diff --git a/thirdparty/photorec_exec/plugins/BartPE/ReadMe.txt b/thirdparty/photorec_exec/bin/plugins/BartPE/ReadMe.txt similarity index 95% rename from thirdparty/photorec_exec/plugins/BartPE/ReadMe.txt rename to thirdparty/photorec_exec/bin/plugins/BartPE/ReadMe.txt index 911ee64667..f033671e24 100644 --- a/thirdparty/photorec_exec/plugins/BartPE/ReadMe.txt +++ b/thirdparty/photorec_exec/bin/plugins/BartPE/ReadMe.txt @@ -1,4 +1,4 @@ -TestDisk and PhotoRec v6.14 Plugin +TestDisk and PhotoRec v7.1 Plugin ===================================== Plugin by: Xtreme - Ahmed Hossam Plugin for: Christophe GRENIER (www.cgsecurity.org) diff --git a/thirdparty/photorec_exec/plugins/BartPE/SCRIPTS/Start_INF.dat b/thirdparty/photorec_exec/bin/plugins/BartPE/SCRIPTS/Start_INF.dat similarity index 88% rename from thirdparty/photorec_exec/plugins/BartPE/SCRIPTS/Start_INF.dat rename to thirdparty/photorec_exec/bin/plugins/BartPE/SCRIPTS/Start_INF.dat index 7df06d1196..b6cea34088 100644 --- a/thirdparty/photorec_exec/plugins/BartPE/SCRIPTS/Start_INF.dat +++ b/thirdparty/photorec_exec/bin/plugins/BartPE/SCRIPTS/Start_INF.dat @@ -1,5 +1,5 @@ ; Testdisk.inf -; PE Builder v3 plugin INF file for Testdisk and PhotoRec 6.14 +; PE Builder v3 plugin INF file for Testdisk and PhotoRec 7.1 ; Plugin by Ahmed Hossam/Xtreme ; Made For Christophe GRENIER (www.cgsecurity.org) ; © Windows Xpire Tech Center diff --git a/thirdparty/photorec_exec/plugins/BartPE/SCRIPTS/StaticINF.dat b/thirdparty/photorec_exec/bin/plugins/BartPE/SCRIPTS/StaticINF.dat similarity index 64% rename from thirdparty/photorec_exec/plugins/BartPE/SCRIPTS/StaticINF.dat rename to thirdparty/photorec_exec/bin/plugins/BartPE/SCRIPTS/StaticINF.dat index 5713cb8995..72dcbc0d1d 100644 --- a/thirdparty/photorec_exec/plugins/BartPE/SCRIPTS/StaticINF.dat +++ b/thirdparty/photorec_exec/bin/plugins/BartPE/SCRIPTS/StaticINF.dat @@ -1,5 +1,5 @@ ; Testdisk.inf -; PE Builder v3 plugin INF file for Testdisk and PhotoRec 6.14 +; PE Builder v3 plugin INF file for Testdisk and PhotoRec 7.1 ; Plugin by Ahmed Hossam/Xtreme ; Made For Christophe GRENIER (www.cgsecurity.org) ; © Windows Xpire Tech Center @@ -22,13 +22,31 @@ b="Programs\testdisk\63",2 [SourceDisksFiles] files\testdisk_win.exe=a,,1 files\photorec_win.exe=a,,1 +files\qphotorec_win.exe=a,,1 files\fidentify_win.exe=a,,1 +files\cyggcc_s-1.dll=a,,1 +files\cygiconv-2.dll=a,,1 +files\cygjpeg-8.dll=a,,1 +files\cygncursesw-10.dll=a,,1 +files\cygssp-0.dll=a,,1 files\cygwin1.dll=a,,1 +files\cygz.dll=a,,1 +files\iconv.dll=a,,1 +files\libgcc_s_sjlj-1.dll=a,,1 +files\libjpeg-62.dll=a,,1 +files\libpng16-16.dll=a,,1 +files\libssp-0.dll=a,,1 +files\libstdc++-6.dll=a,,1 +files\libwinpthread-1.dll=a,,1 +files\QtCore4.dll=a,,1 +files\QtGui4.dll=a,,1 +files\zlib1.dll=a,,1 files\63\cygwin=b,,1 [Software.AddReg] 0x2, "Sherpya\XPEinit\Programs","Disk Tools\TestDisk - Fix MBR and Recover lost partitions","%systemdrive%\Programs\testdisk\testdisk_win.exe" 0x2, "Sherpya\XPEinit\Programs","Disk Tools\PhotoRec - Recover lost files (Doc, Pic and Video)","%systemdrive%\Programs\testdisk\photorec_win.exe" +0x2, "Sherpya\XPEinit\Programs","Disk Tools\QPhotoRec - GUI to recover lost files (Doc, Pic and Video)","%systemdrive%\Programs\testdisk\qphotorec_win.exe" 0x2, "Sherpya\XPEinit\Programs","Disk Tools\files Identify","%systemdrive%\Programs\testdisk\fidentify_win.exe" ; Remove the comment ";" from the below lines if you are going to use NU2 menu diff --git a/thirdparty/photorec_exec/plugins/BartPE/start.inf b/thirdparty/photorec_exec/bin/plugins/BartPE/start.inf similarity index 88% rename from thirdparty/photorec_exec/plugins/BartPE/start.inf rename to thirdparty/photorec_exec/bin/plugins/BartPE/start.inf index 7df06d1196..b6cea34088 100644 --- a/thirdparty/photorec_exec/plugins/BartPE/start.inf +++ b/thirdparty/photorec_exec/bin/plugins/BartPE/start.inf @@ -1,5 +1,5 @@ ; Testdisk.inf -; PE Builder v3 plugin INF file for Testdisk and PhotoRec 6.14 +; PE Builder v3 plugin INF file for Testdisk and PhotoRec 7.1 ; Plugin by Ahmed Hossam/Xtreme ; Made For Christophe GRENIER (www.cgsecurity.org) ; © Windows Xpire Tech Center diff --git a/thirdparty/photorec_exec/plugins/BartPE/testdisk_nu2menu.xml b/thirdparty/photorec_exec/bin/plugins/BartPE/testdisk_nu2menu.xml similarity index 63% rename from thirdparty/photorec_exec/plugins/BartPE/testdisk_nu2menu.xml rename to thirdparty/photorec_exec/bin/plugins/BartPE/testdisk_nu2menu.xml index 9e2495f367..a774a8d89c 100644 --- a/thirdparty/photorec_exec/plugins/BartPE/testdisk_nu2menu.xml +++ b/thirdparty/photorec_exec/bin/plugins/BartPE/testdisk_nu2menu.xml @@ -1,7 +1,8 @@ - + Testdisk - Fix MBR and Recover lost partitions PhotoRec - Recover lost files (Doc, Pic and Video) + QPhotoRec - GUI to recover lost files (Doc, Pic and Video) diff --git a/thirdparty/photorec_exec/plugins/WinBuilder/Help.htm b/thirdparty/photorec_exec/bin/plugins/WinBuilder/Help.htm similarity index 93% rename from thirdparty/photorec_exec/plugins/WinBuilder/Help.htm rename to thirdparty/photorec_exec/bin/plugins/WinBuilder/Help.htm index be824606e9..a5507f7b0b 100644 --- a/thirdparty/photorec_exec/plugins/WinBuilder/Help.htm +++ b/thirdparty/photorec_exec/bin/plugins/WinBuilder/Help.htm @@ -30,7 +30,7 @@

Instructions

    -
  • 1- Download TestDisk and PhotoRec from Here
  • +
  • 1- Download TestDisk and PhotoRec from Here
  • 2- Uncompress the download.
  • 3- Copy TestDisk.script to the WinBuilder App project folder.
  • 4- Start your build and enjoy !
  • @@ -38,7 +38,7 @@

    Support

diff --git a/thirdparty/photorec_exec/plugins/WinBuilder/ReadMe.txt b/thirdparty/photorec_exec/bin/plugins/WinBuilder/ReadMe.txt similarity index 95% rename from thirdparty/photorec_exec/plugins/WinBuilder/ReadMe.txt rename to thirdparty/photorec_exec/bin/plugins/WinBuilder/ReadMe.txt index 0ac9e4b000..4f4b6004f7 100644 --- a/thirdparty/photorec_exec/plugins/WinBuilder/ReadMe.txt +++ b/thirdparty/photorec_exec/bin/plugins/WinBuilder/ReadMe.txt @@ -1,4 +1,4 @@ -TestDisk and PhotoRec v6.14 Plugin +TestDisk and PhotoRec v7.1 Plugin ===================================== Plugin by: Xtreme - Ahmed Hossam Plugin for: Christophe GRENIER (www.cgsecurity.org) diff --git a/thirdparty/photorec_exec/plugins/WinBuilder/TestDisk.script b/thirdparty/photorec_exec/bin/plugins/WinBuilder/TestDisk.script similarity index 75% rename from thirdparty/photorec_exec/plugins/WinBuilder/TestDisk.script rename to thirdparty/photorec_exec/bin/plugins/WinBuilder/TestDisk.script index 2ada4659c2..d1e83c966c 100644 --- a/thirdparty/photorec_exec/plugins/WinBuilder/TestDisk.script +++ b/thirdparty/photorec_exec/bin/plugins/WinBuilder/TestDisk.script @@ -1,12 +1,12 @@ [main] -Title=TestDisk & PhotoRec 6.14 +Title=TestDisk & PhotoRec 7.1 Description=A powerful free data recovery software utility. Selected=True Level=5 Version=1 Author=Xtreme Contact=http://www.boot-land.net/forums/ -Credits=Christophe GRENIER (http://www.cgsecurity.org) +Credits=Christophe GRENIER (https://www.cgsecurity.org) Date=Friday, May 16,2008 [Interface] @@ -26,15 +26,33 @@ StrFormat,CTrim,"%two_folders_up%","\",%two_folders_up% If,NotExistFile,"%two_folders_up%\testdisk_win.exe",Run,%ScriptFile%,Halt DirMake,"%Target_Prog%\%ProgramFolder%" DirMake,"%Target_Prog%\%ProgramFolder%\63" -FileCopy,"%two_folders_up%\cygwin1.dll","%Target_Prog%\%ProgramFolder%" +FileCopy,"%two_folders_up%\cyggcc_s-1.dll","%Target_Prog%%ProgramFolder%" +FileCopy,"%two_folders_up%\cygiconv-2.dll","%Target_Prog%%ProgramFolder%" +FileCopy,"%two_folders_up%\cygjpeg-8.dll","%Target_Prog%%ProgramFolder%" +FileCopy,"%two_folders_up%\cygncursesw-10.dll","%Target_Prog%%ProgramFolder%" +FileCopy,"%two_folders_up%\cygssp-0.dll","%Target_Prog%%ProgramFolder%" +FileCopy,"%two_folders_up%\cygwin1.dll","%Target_Prog%%ProgramFolder%" +FileCopy,"%two_folders_up%\cygz.dll","%Target_Prog%%ProgramFolder%" +FileCopy,"%two_folders_up%\iconv.dll","%Target_Prog%%ProgramFolder%" +FileCopy,"%two_folders_up%\libgcc_s_sjlj-1.dll","%Target_Prog%%ProgramFolder%" +FileCopy,"%two_folders_up%\libjpeg-62.dll","%Target_Prog%%ProgramFolder%" +FileCopy,"%two_folders_up%\libpng16-16.dll","%Target_Prog%%ProgramFolder%" +FileCopy,"%two_folders_up%\libssp-0.dll","%Target_Prog%%ProgramFolder%" +FileCopy,"%two_folders_up%\libstdc++-6.dll","%Target_Prog%%ProgramFolder%" +FileCopy,"%two_folders_up%\libwinpthread-1.dll","%Target_Prog%%ProgramFolder%" +FileCopy,"%two_folders_up%\QtCore4.dll","%Target_Prog%%ProgramFolder%" +FileCopy,"%two_folders_up%\QtGui4.dll","%Target_Prog%%ProgramFolder%" +FileCopy,"%two_folders_up%\zlib1.dll","%Target_Prog%%ProgramFolder%" FileCopy,"%two_folders_up%\fidentify_win.exe","%Target_Prog%\%ProgramFolder%" FileCopy,"%two_folders_up%\photorec_win.exe","%Target_Prog%\%ProgramFolder%" +FileCopy,"%two_folders_up%\qphotorec_win.exe","%Target_Prog%\%ProgramFolder%" FileCopy,"%two_folders_up%\readme.txt","%Target_Prog%\%ProgramFolder%" FileCopy,"%two_folders_up%\testdisk_win.exe","%Target_Prog%\%ProgramFolder%" DirCopy,"%two_folders_up%\63\*.*","%Target_Prog%\%ProgramFolder%\63" ::DirCopy,"%two_folders_up%\TestDisk\*.*","%Target_Prog%\%ProgramFolder%" Add_Shortcut,StartMenu,"%TestDisk & PhotoRec" Add_Shortcut,StartMenu,"TestDisk & PhotoRec","%PE_Programs%\%ProgramFolder%\photorec_win.exe","PhotoRec" +Add_Shortcut,StartMenu,"TestDisk & PhotoRec","%PE_Programs%\%ProgramFolder%\qphotorec_win.exe","QPhotoRec" Add_Shortcut,StartMenu,"TestDisk & PhotoRec","%PE_Programs%\%ProgramFolder%\testdisk_win.exe","Testdisk" Add_Shortcut,StartMenu,"TestDisk & PhotoRec","%PE_Programs%\%ProgramFolder%\fidentify_win.exe","Files Identify" diff --git a/thirdparty/photorec_exec/bin/qphotorec_win.exe b/thirdparty/photorec_exec/bin/qphotorec_win.exe new file mode 100644 index 0000000000..7415ccea4c Binary files /dev/null and b/thirdparty/photorec_exec/bin/qphotorec_win.exe differ diff --git a/thirdparty/photorec_exec/readme.txt b/thirdparty/photorec_exec/bin/readme.txt similarity index 79% rename from thirdparty/photorec_exec/readme.txt rename to thirdparty/photorec_exec/bin/readme.txt index 57cd3b6f3a..1995c1fc42 100644 --- a/thirdparty/photorec_exec/readme.txt +++ b/thirdparty/photorec_exec/bin/readme.txt @@ -12,12 +12,12 @@ For Windows 64-bit without WoW64, use the Windows 64-bit version of TestDisk & PhotoRec. If you are using an older version of Windows, run the DOS version of TestDisk. -You can download it from http://www.cgsecurity.org/wiki/TestDisk_Download +You can download it from https://www.cgsecurity.org/wiki/TestDisk_Download TestDisk doesn't need to be installed, you only need to - extract the files - run testdisk_win.exe or photorec_win.exe TestDisk & PhotoRec documentation can be found online: -- http://www.cgsecurity.org/wiki/TestDisk -- http://www.cgsecurity.org/wiki/PhotoRec +- https://www.cgsecurity.org/wiki/TestDisk +- https://www.cgsecurity.org/wiki/PhotoRec diff --git a/thirdparty/photorec_exec/bin/testdisk.pdf b/thirdparty/photorec_exec/bin/testdisk.pdf new file mode 100644 index 0000000000..c8ccc59522 Binary files /dev/null and b/thirdparty/photorec_exec/bin/testdisk.pdf differ diff --git a/thirdparty/photorec_exec/bin/testdisk_win.exe b/thirdparty/photorec_exec/bin/testdisk_win.exe new file mode 100644 index 0000000000..0fd829923b Binary files /dev/null and b/thirdparty/photorec_exec/bin/testdisk_win.exe differ diff --git a/thirdparty/photorec_exec/bin/zlib1.dll b/thirdparty/photorec_exec/bin/zlib1.dll new file mode 100644 index 0000000000..75aa81eab3 Binary files /dev/null and b/thirdparty/photorec_exec/bin/zlib1.dll differ diff --git a/thirdparty/photorec_exec/cyggcc_s-1.dll b/thirdparty/photorec_exec/cyggcc_s-1.dll deleted file mode 100755 index d2ea601c9f..0000000000 Binary files a/thirdparty/photorec_exec/cyggcc_s-1.dll and /dev/null differ diff --git a/thirdparty/photorec_exec/cygjpeg-8.dll b/thirdparty/photorec_exec/cygjpeg-8.dll deleted file mode 100755 index 7f1f2eb953..0000000000 Binary files a/thirdparty/photorec_exec/cygjpeg-8.dll and /dev/null differ diff --git a/thirdparty/photorec_exec/cygncursesw-10.dll b/thirdparty/photorec_exec/cygncursesw-10.dll deleted file mode 100755 index 7f105b32b7..0000000000 Binary files a/thirdparty/photorec_exec/cygncursesw-10.dll and /dev/null differ diff --git a/thirdparty/photorec_exec/cygssp-0.dll b/thirdparty/photorec_exec/cygssp-0.dll deleted file mode 100755 index 23bd09df51..0000000000 Binary files a/thirdparty/photorec_exec/cygssp-0.dll and /dev/null differ diff --git a/thirdparty/photorec_exec/cygwin1.dll b/thirdparty/photorec_exec/cygwin1.dll deleted file mode 100755 index 12a08b7adc..0000000000 Binary files a/thirdparty/photorec_exec/cygwin1.dll and /dev/null differ diff --git a/thirdparty/photorec_exec/cygz.dll b/thirdparty/photorec_exec/cygz.dll deleted file mode 100755 index fe28c63335..0000000000 Binary files a/thirdparty/photorec_exec/cygz.dll and /dev/null differ diff --git a/thirdparty/photorec_exec/documentation.html b/thirdparty/photorec_exec/documentation.html deleted file mode 100644 index e4f0877567..0000000000 --- a/thirdparty/photorec_exec/documentation.html +++ /dev/null @@ -1,12 +0,0 @@ - - -TestDisk & PhotoRec documentation - - -TestDisk & PhotoRec documentation can be found online: - - - diff --git a/thirdparty/photorec_exec/fidentify_win.exe b/thirdparty/photorec_exec/fidentify_win.exe deleted file mode 100755 index eff25a8065..0000000000 Binary files a/thirdparty/photorec_exec/fidentify_win.exe and /dev/null differ diff --git a/thirdparty/photorec_exec/iconv.dll b/thirdparty/photorec_exec/iconv.dll deleted file mode 100755 index 12140fcdf6..0000000000 Binary files a/thirdparty/photorec_exec/iconv.dll and /dev/null differ diff --git a/thirdparty/photorec_exec/libgcc_s_sjlj-1.dll b/thirdparty/photorec_exec/libgcc_s_sjlj-1.dll deleted file mode 100755 index 358b42e762..0000000000 Binary files a/thirdparty/photorec_exec/libgcc_s_sjlj-1.dll and /dev/null differ diff --git a/thirdparty/photorec_exec/libjpeg-62.dll b/thirdparty/photorec_exec/libjpeg-62.dll deleted file mode 100755 index 376a18e42f..0000000000 Binary files a/thirdparty/photorec_exec/libjpeg-62.dll and /dev/null differ diff --git a/thirdparty/photorec_exec/libpng16-16.dll b/thirdparty/photorec_exec/libpng16-16.dll deleted file mode 100755 index e1c18e9bcd..0000000000 Binary files a/thirdparty/photorec_exec/libpng16-16.dll and /dev/null differ diff --git a/thirdparty/photorec_exec/libssp-0.dll b/thirdparty/photorec_exec/libssp-0.dll deleted file mode 100755 index 7a87258557..0000000000 Binary files a/thirdparty/photorec_exec/libssp-0.dll and /dev/null differ diff --git a/thirdparty/photorec_exec/libstdc++-6.dll b/thirdparty/photorec_exec/libstdc++-6.dll deleted file mode 100755 index 00a52cb5df..0000000000 Binary files a/thirdparty/photorec_exec/libstdc++-6.dll and /dev/null differ diff --git a/thirdparty/photorec_exec/libwinpthread-1.dll b/thirdparty/photorec_exec/libwinpthread-1.dll deleted file mode 100755 index bc94f0183e..0000000000 Binary files a/thirdparty/photorec_exec/libwinpthread-1.dll and /dev/null differ diff --git a/thirdparty/photorec_exec/photorec_win.exe b/thirdparty/photorec_exec/photorec_win.exe deleted file mode 100755 index e3fb92dec8..0000000000 Binary files a/thirdparty/photorec_exec/photorec_win.exe and /dev/null differ diff --git a/thirdparty/photorec_exec/qphotorec_win.exe b/thirdparty/photorec_exec/qphotorec_win.exe deleted file mode 100755 index 7d7931cd09..0000000000 Binary files a/thirdparty/photorec_exec/qphotorec_win.exe and /dev/null differ diff --git a/thirdparty/photorec_exec/testdisk_win.exe b/thirdparty/photorec_exec/testdisk_win.exe deleted file mode 100755 index 7455bd0657..0000000000 Binary files a/thirdparty/photorec_exec/testdisk_win.exe and /dev/null differ diff --git a/thirdparty/photorec_exec/zlib1.dll b/thirdparty/photorec_exec/zlib1.dll deleted file mode 100755 index b39de53bf3..0000000000 Binary files a/thirdparty/photorec_exec/zlib1.dll and /dev/null differ diff --git a/thirdparty/rr-full/plugins/eventlogs.pl b/thirdparty/rr-full/plugins/eventlogs.pl index 200bb07d7f..f95ebbc888 100644 --- a/thirdparty/rr-full/plugins/eventlogs.pl +++ b/thirdparty/rr-full/plugins/eventlogs.pl @@ -57,9 +57,11 @@ sub pluginmain { ::rptMsg($win_path); ::rptMsg("LastWrite Time ".gmtime($win->get_timestamp())." (UTC)"); my $cn; - if ($cn = $win->get_value("ComputerName")->get_data()) { - ::rptMsg("ComputerName = ".$cn); - } + if (defined($win->get_value("ComputerName"))) { + if ($cn = $win->get_value("ComputerName")->get_data()) { + ::rptMsg("ComputerName = ".$cn); + } + } else { ::rptMsg("ComputerName value not found."); } @@ -81,9 +83,15 @@ sub pluginmain { ::rptMsg(" ".$evpath); ::rptMsg(" LastWrite Time ".gmtime($evlog->get_timestamp())." (UTC)"); ::rptMsg(" Configuration Settings"); - ::rptMsg(" Log location: ".$evlog->get_value('File')->get_data()); - ::rptMsg(" Log Size: ".$evlog->get_value('MaxSize')->get_data()." Bytes"); - ($evlog->get_value('AutoBackupLogFiles') == 0x0) ? ::rptMsg(" AutoBackupLogFiles is Disabled") : ::rptMsg(" AutoBackupLogFiles is Enabled") + if (defined($evlog->get_value('File'))) { + ::rptMsg(" Log location: ".$evlog->get_value('File')->get_data()); + } + if (defined($evlog->get_value('MaxSize'))) { + ::rptMsg(" Log Size: ".$evlog->get_value('MaxSize')->get_data()." Bytes"); + } + if (defined($evlog->get_value('AutoBackupLogFiles'))) { + ($evlog->get_value('AutoBackupLogFiles') == 0x0) ? ::rptMsg(" AutoBackupLogFiles is Disabled") : ::rptMsg(" AutoBackupLogFiles is Enabled") + } } else { ::rptMsg($logname->get_name()." Event Log not found."); diff --git a/thirdparty/rr-full/plugins/ie_zones.pl b/thirdparty/rr-full/plugins/ie_zones.pl index 935b3fb537..a78831049f 100644 --- a/thirdparty/rr-full/plugins/ie_zones.pl +++ b/thirdparty/rr-full/plugins/ie_zones.pl @@ -60,40 +60,48 @@ sub pluginmain { ::rptMsg(""); # Get Zones and various security settings foreach my $n (0..4) { - $zone = $key->get_subkey('Zones\\'.$n); - ::rptMsg("Zone ".$n.": ".$zone->get_value("PMDisplayName")->get_data()." - ".$zone->get_value("Description")->get_data()); - ::rptMsg("LastWrite: ".gmtime($zone->get_timestamp()." UTC")); - - my @vals = $zone->get_list_of_values(); - if (scalar(@vals) > 0) { - foreach my $v (@vals) { - my $name = $v->get_name(); - next unless (length($name) == 4 && $name ne "Icon"); - my $data = $v->get_data(); - $name = "**".$name if ($name eq "1609" && $data == 0); - my $str = sprintf "%6s 0x%08x",$name,$data; -# ::rptMsg(" ".$name." ".$data." ".$zones{$data}); - ::rptMsg($str." ".$zones{$data}); - } - } - ::rptMsg(""); + if (defined($key->get_subkey('Zones\\'.$n))) { + $zone = $key->get_subkey('Zones\\'.$n); + if (defined($zone->get_value("PMDisplayName"))) { + ::rptMsg("Zone ".$n.": ".$zone->get_value("PMDisplayName")->get_data()." - ".$zone->get_value("Description")->get_data()); + } else { + ::rptMsg("Zone ".$n.": ".$zone->get_value("DisplayName")->get_data()." - ".$zone->get_value("Description")->get_data()); + } + ::rptMsg("LastWrite: ".gmtime($zone->get_timestamp()." UTC")); + + my @vals = $zone->get_list_of_values(); + if (scalar(@vals) > 0) { + foreach my $v (@vals) { + my $name = $v->get_name(); + next unless (length($name) == 4 && $name ne "Icon"); + my $data = $v->get_data(); + $name = "**".$name if ($name eq "1609" && $data == 0); + my $str = sprintf "%6s 0x%08x",$name,$data; + # ::rptMsg(" ".$name." ".$data." ".$zones{$data}); + ::rptMsg($str." ".$zones{$data}); + } + } + ::rptMsg(""); + } } # Now, get ZoneMap settings - my $zonemap = $key->get_subkey('ZoneMap\\Domains'); - my @domains = $zonemap->get_list_of_subkeys(); - if (scalar(@domains) > 0) { - foreach my $d (@domains) { - ::rptMsg("Domain: ".$d->get_name()); + if (defined($key->get_subkey('ZoneMap\\Domains'))) { + my $zonemap = $key->get_subkey('ZoneMap\\Domains'); + my @domains = $zonemap->get_list_of_subkeys(); + if (scalar(@domains) > 0) { + foreach my $d (@domains) { + ::rptMsg("Domain: ".$d->get_name()); - my @vals = $d->get_list_of_values(); - if (scalar(@vals) > 0) { - foreach my $v (@vals) { - ::rptMsg(" ".$v->get_name()." ".$v->get_data()); - } - } - ::rptMsg(""); - } - } + my @vals = $d->get_list_of_values(); + if (scalar(@vals) > 0) { + foreach my $v (@vals) { + ::rptMsg(" ".$v->get_name()." ".$v->get_data()); + } + } + ::rptMsg(""); + } + } + } } else { # ::rptMsg($key_path." not found."); diff --git a/thirdparty/rr-full/plugins/searchscopes.pl b/thirdparty/rr-full/plugins/searchscopes.pl index 8b8eecc9c0..299293f94f 100644 --- a/thirdparty/rr-full/plugins/searchscopes.pl +++ b/thirdparty/rr-full/plugins/searchscopes.pl @@ -47,8 +47,10 @@ sub pluginmain { if ($key = $root_key->get_subkey($key_path)) { ::rptMsg("SearchScopes"); ::rptMsg($key_path); - ::rptMsg("DefaultScope: ".$key->get_value("DefaultScope")->get_data()); - ::rptMsg(""); + if (defined($key->get_value("DefaultScope"))) { + ::rptMsg("DefaultScope: ".$key->get_value("DefaultScope")->get_data()); + ::rptMsg(""); + } # ::rptMsg("LastWrite Time ".gmtime($key->get_timestamp())." (UTC)"); my @subkeys = $key->get_list_of_subkeys(); if (scalar(@subkeys) > 0) { diff --git a/thirdparty/rr-full/plugins/system b/thirdparty/rr-full/plugins/system index f95897e19d..568b92c648 100644 --- a/thirdparty/rr-full/plugins/system +++ b/thirdparty/rr-full/plugins/system @@ -4,6 +4,7 @@ appcompatcache auditfail backuprestore bam +bam_tln bthport comfoo compname diff --git a/thirdparty/rr-full/shellitems.pl b/thirdparty/rr-full/shellitems.pl index ec39234690..34b9174c9d 100644 --- a/thirdparty/rr-full/shellitems.pl +++ b/thirdparty/rr-full/shellitems.pl @@ -480,10 +480,11 @@ sub parseDeviceEntry { # Version 3 = XP if ($ver == 3) { - my $guid1 = parseGUID(substr($data,$ofs + 6,16)); + if (length($data) > ($ofs + 6)) { + my $guid1 = parseGUID(substr($data,$ofs + 6,16)); my $guid2 = parseGUID(substr($data,$ofs + 6 + 16,16)); $item{name} = $guid1."\\".$guid2 - + } } # Version 8 = Win7 elsif ($ver == 8) { diff --git a/thirdparty/rr/plugins/arunmru.pl b/thirdparty/rr/plugins/arunmru.pl index 504700f145..9d8ed281bd 100644 --- a/thirdparty/rr/plugins/arunmru.pl +++ b/thirdparty/rr/plugins/arunmru.pl @@ -36,43 +36,44 @@ sub pluginmain { my $class = shift; my $ntuser = shift; #::logMsg("autospyrunmru"); - my $reg = Parse::Win32Registry->new($ntuser); - my $root_key = $reg->get_root_key; + if (defined(Parse::Win32Registry->new($ntuser))) { + my $reg = Parse::Win32Registry->new($ntuser); + my $root_key = $reg->get_root_key; - my $key_path = 'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\RunMRU'; - my $key; - if ($key = $root_key->get_subkey($key_path)) { - #::rptMsg("RunMru"); - #::rptMsg($key_path); - - my @vals = $key->get_list_of_values(); - ::rptMsg(""); - ::rptMsg("".gmtime($key->get_timestamp()).""); - ::rptMsg(""); - my %runvals; - my $mru; - if (scalar(@vals) > 0) { - foreach my $v (@vals) { - $runvals{$v->get_name()} = $v->get_data() unless ($v->get_name() =~ m/^MRUList/i); - $mru = $v->get_data() if ($v->get_name() =~ m/^MRUList/i); - } - ::rptMsg("".$mru.""); - foreach my $r (sort keys %runvals) { - ::rptMsg("".$r." ".$runvals{$r}.""); - } - } - else { - #::rptMsg($key_path." has no values."); - #::logMsg($key_path." has no values."); - } - ::rptMsg(""); - ::rptMsg(""); - } - else { - #::rptMsg($key_path." not found."); - #::logMsg($key_path." not found."); - } - + my $key_path = 'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\RunMRU'; + my $key; + if ($key = $root_key->get_subkey($key_path)) { + #::rptMsg("RunMru"); + #::rptMsg($key_path); + + my @vals = $key->get_list_of_values(); + ::rptMsg(""); + ::rptMsg("".gmtime($key->get_timestamp()).""); + ::rptMsg(""); + my %runvals; + my $mru; + if (scalar(@vals) > 0) { + foreach my $v (@vals) { + $runvals{$v->get_name()} = $v->get_data() unless ($v->get_name() =~ m/^MRUList/i); + $mru = $v->get_data() if ($v->get_name() =~ m/^MRUList/i); + } + ::rptMsg("".$mru.""); + foreach my $r (sort keys %runvals) { + ::rptMsg("".$r." ".$runvals{$r}.""); + } + } + else { + #::rptMsg($key_path." has no values."); + #::logMsg($key_path." has no values."); + } + ::rptMsg(""); + ::rptMsg(""); + } + else { + #::rptMsg($key_path." not found."); + #::logMsg($key_path." not found."); + } + } } 1; diff --git a/thirdparty/rr/plugins/autopsylogin.pl b/thirdparty/rr/plugins/autopsylogin.pl index ab0365817e..2a72ba6936 100644 --- a/thirdparty/rr/plugins/autopsylogin.pl +++ b/thirdparty/rr/plugins/autopsylogin.pl @@ -35,36 +35,38 @@ sub pluginmain { my $class = shift; my $ntuser = shift; #::logMsg("||logonusername||"); - my $reg = Parse::Win32Registry->new($ntuser); - my $root_key = $reg->get_root_key; - - my $logon_name = "Username"; - - my $key_path = 'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer'; - my $key; - if ($key = $root_key->get_subkey($key_path)) { - my @vals = $key->get_list_of_values(); - if (scalar(@vals) > 0) { - #::rptMsg("Logon User Name"); - #::rptMsg($key_path); - ::rptMsg(""); - ::rptMsg("".gmtime($key->get_timestamp()).""); - foreach my $v (@vals) { - if ($v->get_name() eq $logon_name) { - ::rptMsg(" ".$v->get_data() .""); - } - } - ::rptMsg(""); - } - else { - #::rptMsg($key_path." has no values."); - #::logMsg($key_path." has no values."); - } - } - else { - #::rptMsg($key_path." not found."); - #::logMsg($key_path." not found."); - } + if (defined(Parse::Win32Registry->new($ntuser))) { + my $reg = Parse::Win32Registry->new($ntuser); + my $root_key = $reg->get_root_key; + + my $logon_name = "Username"; + + my $key_path = 'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer'; + my $key; + if ($key = $root_key->get_subkey($key_path)) { + my @vals = $key->get_list_of_values(); + if (scalar(@vals) > 0) { + #::rptMsg("Logon User Name"); + #::rptMsg($key_path); + ::rptMsg(""); + ::rptMsg("".gmtime($key->get_timestamp()).""); + foreach my $v (@vals) { + if ($v->get_name() eq $logon_name) { + ::rptMsg(" ".$v->get_data() .""); + } + } + ::rptMsg(""); + } + else { + #::rptMsg($key_path." has no values."); + #::logMsg($key_path." has no values."); + } + } + else { + #::rptMsg($key_path." not found."); + #::logMsg($key_path." not found."); + } + } } 1; diff --git a/thirdparty/rr/plugins/autopsyntusernetwork.pl b/thirdparty/rr/plugins/autopsyntusernetwork.pl index 715e89b8ff..910679be95 100644 --- a/thirdparty/rr/plugins/autopsyntusernetwork.pl +++ b/thirdparty/rr/plugins/autopsyntusernetwork.pl @@ -30,64 +30,67 @@ sub pluginmain { my $ntuser = shift; #::logMsg("Launching ntusernetwork v.".$VERSION); #::rptMsg("ntusernetwork v.".$VERSION); # banner - #::rptMsg("(".$config{hive}.") ".getShortDescr()."\n"); # banner - my $reg = Parse::Win32Registry->new($ntuser); - my $root_key = $reg->get_root_key; - - ::rptMsg(""); - ::rptMsg(""); - ::rptMsg(""); + #::rptMsg("(".$config{hive}.") ".getShortDescr()."\n"); # banner + if (defined(Parse::Win32Registry->new($ntuser))) { + + my $reg = Parse::Win32Registry->new($ntuser); + my $root_key = $reg->get_root_key; + + ::rptMsg(""); + ::rptMsg(""); + ::rptMsg(""); - my $key_path = 'Network'; - my $key; - if ($key = $root_key->get_subkey($key_path)) { + my $key_path = 'Network'; + my $key; + if ($key = $root_key->get_subkey($key_path)) { - my @subkeys = $key->get_list_of_subkeys(); - if (scalar @subkeys > 0) { - foreach my $s (@subkeys) { - #::rptMsg($key_path."\\".$s->get_name()); - my $localPath = $key_path."\\".$s->get_name(); + my @subkeys = $key->get_list_of_subkeys(); + if (scalar @subkeys > 0) { + foreach my $s (@subkeys) { + #::rptMsg($key_path."\\".$s->get_name()); + my $localPath = $key_path."\\".$s->get_name(); - my $remotePath; - eval { - $remotePath = $s->get_value("RemotePath")->get_data(); - }; - if ($@) { - # ::rptMsg("OS value not found."); - } - else { - ::rptMsg("". $remotePath . ""); - } - } - } + my $remotePath; + eval { + $remotePath = $s->get_value("RemotePath")->get_data(); + }; + if ($@) { + # ::rptMsg("OS value not found."); + } + else { + ::rptMsg("". $remotePath . ""); + } + } + } - # ::rptMsg($key_path); - # ::rptMsg(""); + # ::rptMsg($key_path); + # ::rptMsg(""); - - # my @subkeys = $key->get_list_of_subkeys(); - # if (scalar @subkeys > 0) { - # foreach my $s (@subkeys) { - # ::rptMsg($key_path."\\".$s->get_name()); - # ::rptMsg("LastWrite time: ".gmtime($s->get_timestamp())); - # my @vals = $s->get_list_of_values(); - # if (scalar @vals > 0) { - # foreach my $v (@vals) { - # ::rptMsg(sprintf " %-15s %-25s",$v->get_name(),$v->get_data()); - # } - # ::rptMsg(""); - # } - # } - # } - # else { - # ::rptMsg($key_path." key has no subkeys."); - # } - } - else { - #::rptMsg($key_path." key not found."); - } - ::rptMsg(""); + + # my @subkeys = $key->get_list_of_subkeys(); + # if (scalar @subkeys > 0) { + # foreach my $s (@subkeys) { + # ::rptMsg($key_path."\\".$s->get_name()); + # ::rptMsg("LastWrite time: ".gmtime($s->get_timestamp())); + # my @vals = $s->get_list_of_values(); + # if (scalar @vals > 0) { + # foreach my $v (@vals) { + # ::rptMsg(sprintf " %-15s %-25s",$v->get_name(),$v->get_data()); + # } + # ::rptMsg(""); + # } + # } + # } + # else { + # ::rptMsg($key_path." key has no subkeys."); + # } + } + else { + #::rptMsg($key_path." key not found."); + } + ::rptMsg(""); + } } 1; diff --git a/thirdparty/rr/plugins/autopsyrecentdocs.pl b/thirdparty/rr/plugins/autopsyrecentdocs.pl index 776126175b..e2a05aa699 100644 --- a/thirdparty/rr/plugins/autopsyrecentdocs.pl +++ b/thirdparty/rr/plugins/autopsyrecentdocs.pl @@ -41,80 +41,82 @@ sub pluginmain { my $class = shift; my $ntuser = shift; #::logMsg("||recentdocs||"); - my $reg = Parse::Win32Registry->new($ntuser); - my $root_key = $reg->get_root_key; - my $key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\RecentDocs"; - my $key; - if ($key = $root_key->get_subkey($key_path)) { - #::rptMsg("RecentDocs"); - #::rptMsg("**All values printed in MRUList\\MRUListEx order."); - #::rptMsg($key_path); - ::rptMsg("".gmtime($key->get_timestamp()).""); -# Get RecentDocs values - my %rdvals = getRDValues($key); - if (%rdvals) { - my $tag; - if (exists $rdvals{"MRUListEx"}) { - $tag = "MRUListEx"; - } - elsif (exists $rdvals{"MRUList"}) { - $tag = "MRUList"; - } - else { - - } - - my @list = split(/,/,$rdvals{$tag}); - foreach my $i (@list) { - ::rptMsg("".$rdvals{$i} . ""); - } - - } - else { - #::rptMsg($key_path." has no values."); - #::logMsg("Error: ".$key_path." has no values."); - } - ::rptMsg(""); -# Get RecentDocs subkeys' values - my @subkeys = $key->get_list_of_subkeys(); - if (scalar(@subkeys) > 0) { - foreach my $s (@subkeys) { - #::rptMsg($key_path."\\".$s->get_name()); - #::rptMsg("LastWrite Time ".gmtime($s->get_timestamp())." (UTC)"); - - my %rdvals = getRDValues($s); - if (%rdvals) { - my $tag; - if (exists $rdvals{"MRUListEx"}) { - $tag = "MRUListEx"; - } - elsif (exists $rdvals{"MRUList"}) { - $tag = "MRUList"; - } - else { - - } - - my @list = split(/,/,$rdvals{$tag}); - #::rptMsg($tag." = ".$rdvals{$tag}); - foreach my $i (@list) { - #::rptMsg("".$rdvals{$i}); - } - - #::rptMsg(""); - } - else { - #::rptMsg($key_path." has no values."); - } - } - } - else { - #::rptMsg($key_path." has no subkeys."); - } - } - else { - #::rptMsg($key_path." not found."); - } + if (defined(Parse::Win32Registry->new($ntuser))) { + my $reg = Parse::Win32Registry->new($ntuser); + my $root_key = $reg->get_root_key; + my $key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\RecentDocs"; + my $key; + if ($key = $root_key->get_subkey($key_path)) { + #::rptMsg("RecentDocs"); + #::rptMsg("**All values printed in MRUList\\MRUListEx order."); + #::rptMsg($key_path); + ::rptMsg("".gmtime($key->get_timestamp()).""); + # Get RecentDocs values + my %rdvals = getRDValues($key); + if (%rdvals) { + my $tag; + if (exists $rdvals{"MRUListEx"}) { + $tag = "MRUListEx"; + } + elsif (exists $rdvals{"MRUList"}) { + $tag = "MRUList"; + } + else { + + } + + my @list = split(/,/,$rdvals{$tag}); + foreach my $i (@list) { + ::rptMsg("".$rdvals{$i} . ""); + } + + } + else { + #::rptMsg($key_path." has no values."); + #::logMsg("Error: ".$key_path." has no values."); + } + ::rptMsg(""); + # Get RecentDocs subkeys' values + my @subkeys = $key->get_list_of_subkeys(); + if (scalar(@subkeys) > 0) { + foreach my $s (@subkeys) { + #::rptMsg($key_path."\\".$s->get_name()); + #::rptMsg("LastWrite Time ".gmtime($s->get_timestamp())." (UTC)"); + + my %rdvals = getRDValues($s); + if (%rdvals) { + my $tag; + if (exists $rdvals{"MRUListEx"}) { + $tag = "MRUListEx"; + } + elsif (exists $rdvals{"MRUList"}) { + $tag = "MRUList"; + } + else { + + } + + my @list = split(/,/,$rdvals{$tag}); + #::rptMsg($tag." = ".$rdvals{$tag}); + foreach my $i (@list) { + #::rptMsg("".$rdvals{$i}); + } + + #::rptMsg(""); + } + else { + #::rptMsg($key_path." has no values."); + } + } + } + else { + #::rptMsg($key_path." has no subkeys."); + } + } + else { + #::rptMsg($key_path." not found."); + } + } } diff --git a/thirdparty/rr/plugins/autopsyshellfolders.pl b/thirdparty/rr/plugins/autopsyshellfolders.pl index d625820ec5..01a5b22e6a 100644 --- a/thirdparty/rr/plugins/autopsyshellfolders.pl +++ b/thirdparty/rr/plugins/autopsyshellfolders.pl @@ -41,32 +41,35 @@ sub pluginmain { my $class = shift; my $hive = shift; #::logMsg("Launching shellfolders v.".$VERSION); - my $reg = Parse::Win32Registry->new($hive); - my $root_key = $reg->get_root_key; + if (defined(Parse::Win32Registry->new($hive))) { + my $reg = Parse::Win32Registry->new($hive); - my $key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders"; - my $key; - if ($key = $root_key->get_subkey($key_path)) { - ::rptMsg(""); - ::rptMsg("".gmtime($key->get_timestamp()).""); - - my @vals = $key->get_list_of_values(); - ::rptMsg(""); - if (scalar(@vals) > 0) { - foreach my $v (@vals) { - my $str = sprintf "%-20s %-40s","get_name()."\">",$v->get_data().""; - ::rptMsg($str); - } - ::rptMsg(""); - } - else { - #::rptMsg($key_path." has no values."); - } - ::rptMsg(""); - } - else { - #::rptMsg($key_path." not found."); - #::logMsg($key_path." not found."); - } + my $root_key = $reg->get_root_key; + + my $key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders"; + my $key; + if ($key = $root_key->get_subkey($key_path)) { + ::rptMsg(""); + ::rptMsg("".gmtime($key->get_timestamp()).""); + + my @vals = $key->get_list_of_values(); + ::rptMsg(""); + if (scalar(@vals) > 0) { + foreach my $v (@vals) { + my $str = sprintf "%-20s %-40s","get_name()."\">",$v->get_data().""; + ::rptMsg($str); + } + ::rptMsg(""); + } + else { + #::rptMsg($key_path." has no values."); + } + ::rptMsg(""); + } + else { + #::rptMsg($key_path." not found."); + #::logMsg($key_path." not found."); + } + } } 1; diff --git a/thirdparty/rr/plugins/officedocs.pl b/thirdparty/rr/plugins/officedocs.pl index 707a5c254f..fac96f52ff 100644 --- a/thirdparty/rr/plugins/officedocs.pl +++ b/thirdparty/rr/plugins/officedocs.pl @@ -37,115 +37,117 @@ sub pluginmain { # ::rptMsg("officedocs v.".$VERSION); # 20110830 [fpi] + banner # ::rptMsg("(".getHive().") ".getShortDescr()."\n"); # 20110830 [fpi] + banner ::rptMsg(""); - my $reg = Parse::Win32Registry->new($ntuser); - my $root_key = $reg->get_root_key; - #::rptMsg("officedocs v.".$VERSION); -# First, let's find out which version of Office is installed - my $version; - my $tag = 0; - my @versions = ("7\.0","8\.0", "9\.0", "10\.0", "11\.0","12\.0"); - foreach my $ver (@versions) { - my $key_path = "Software\\Microsoft\\Office\\".$ver."\\Common\\Open Find"; - if (defined($root_key->get_subkey($key_path))) { - $version = $ver; - $tag = 1; - } - } - - if ($tag) { - #::rptMsg("MSOffice version ".$version." located."); - my $key_path = "Software\\Microsoft\\Office\\".$version; - my $of_key = $root_key->get_subkey($key_path); - ::rptMsg(" ".gmtime($of_key->get_timestamp()).""); - ::rptMsg(""); - if ($of_key) { -# Attempt to retrieve Word docs - my @funcs = ("Open","Save As","File Save"); - foreach my $func (@funcs) { - my $word = "Common\\Open Find\\Microsoft Office Word\\Settings\\".$func."\\File Name MRU"; - my $word_key = $of_key->get_subkey($word); - if ($word_key) { - #::rptMsg($word); - - #::rptMsg(""); - my $value = $word_key->get_value("Value")->get_data(); - my @data = split(/\00/,$value); - ::rptMsg("". @data . ""); - #map{::rptMsg("$_");}@data; - } - else { -# ::rptMsg("Could not access ".$word); - } - #::rptMsg(""); - } -# Attempt to retrieve Excel docs - my $excel = 'Excel\\Recent Files'; - if (my $excel_key = $of_key->get_subkey($excel)) { - #::rptMsg($key_path."\\".$excel); - #::rptMsg("LastWrite Time ".gmtime($excel_key->get_timestamp())." (UTC)"); - my @vals = $excel_key->get_list_of_values(); - if (scalar(@vals) > 0) { - my %files; -# Retrieve values and load into a hash for sorting - foreach my $v (@vals) { - my $val = $v->get_name(); - my $data = $v->get_data(); - my $tag = (split(/File/,$val))[1]; - $files{$tag} = $val.":".$data; - } -# Print sorted content to report file - foreach my $u (sort {$a <=> $b} keys %files) { - my ($val,$data) = split(/:/,$files{$u},2); - ::rptMsg("".$data . ""); - } - } - else { - #::rptMsg($key_path.$excel." has no values."); - } - } - else { - #::rptMsg($key_path.$excel." not found."); - } - #::rptMsg(""); -# Attempt to retrieve PowerPoint docs - my $ppt = 'PowerPoint\\Recent File List'; - if (my $ppt_key = $of_key->get_subkey($ppt)) { - #::rptMsg($key_path."\\".$ppt); - #::rptMsg("LastWrite Time ".gmtime($ppt_key->get_timestamp())." (UTC)"); - my @vals = $ppt_key->get_list_of_values(); - if (scalar(@vals) > 0) { - my %files; -# Retrieve values and load into a hash for sorting - foreach my $v (@vals) { - my $val = $v->get_name(); - my $data = $v->get_data(); - my $tag = (split(/File/,$val))[1]; - $files{$tag} = $val.":".$data; - } -# Print sorted content to report file - foreach my $u (sort {$a <=> $b} keys %files) { - my ($val,$data) = split(/:/,$files{$u},2); - ::rptMsg("".$data . ""); - } - } - else { - #::rptMsg($key_path."\\".$ppt." has no values."); - } - } - else { - #::rptMsg($key_path."\\".$ppt." not found."); - } - } - else { - #::rptMsg("Could not access ".$key_path); - #::logMsg("Could not access ".$key_path); - } - ::rptMsg(""); - } - else { - #::logMsg("MSOffice version not found."); - #::rptMsg("MSOffice version not found."); - } + if (defined(Parse::Win32Registry->new($ntuser))) { + my $reg = Parse::Win32Registry->new($ntuser); + my $root_key = $reg->get_root_key; + #::rptMsg("officedocs v.".$VERSION); + # First, let's find out which version of Office is installed + my $version; + my $tag = 0; + my @versions = ("7\.0","8\.0", "9\.0", "10\.0", "11\.0","12\.0"); + foreach my $ver (@versions) { + my $key_path = "Software\\Microsoft\\Office\\".$ver."\\Common\\Open Find"; + if (defined($root_key->get_subkey($key_path))) { + $version = $ver; + $tag = 1; + } + } + + if ($tag) { + #::rptMsg("MSOffice version ".$version." located."); + my $key_path = "Software\\Microsoft\\Office\\".$version; + my $of_key = $root_key->get_subkey($key_path); + ::rptMsg(" ".gmtime($of_key->get_timestamp()).""); + ::rptMsg(""); + if ($of_key) { + # Attempt to retrieve Word docs + my @funcs = ("Open","Save As","File Save"); + foreach my $func (@funcs) { + my $word = "Common\\Open Find\\Microsoft Office Word\\Settings\\".$func."\\File Name MRU"; + my $word_key = $of_key->get_subkey($word); + if ($word_key) { + #::rptMsg($word); + + #::rptMsg(""); + my $value = $word_key->get_value("Value")->get_data(); + my @data = split(/\00/,$value); + ::rptMsg("". @data . ""); + #map{::rptMsg("$_");}@data; + } + else { + # ::rptMsg("Could not access ".$word); + } + #::rptMsg(""); + } + # Attempt to retrieve Excel docs + my $excel = 'Excel\\Recent Files'; + if (my $excel_key = $of_key->get_subkey($excel)) { + #::rptMsg($key_path."\\".$excel); + #::rptMsg("LastWrite Time ".gmtime($excel_key->get_timestamp())." (UTC)"); + my @vals = $excel_key->get_list_of_values(); + if (scalar(@vals) > 0) { + my %files; + # Retrieve values and load into a hash for sorting + foreach my $v (@vals) { + my $val = $v->get_name(); + my $data = $v->get_data(); + my $tag = (split(/File/,$val))[1]; + $files{$tag} = $val.":".$data; + } + # Print sorted content to report file + foreach my $u (sort {$a <=> $b} keys %files) { + my ($val,$data) = split(/:/,$files{$u},2); + ::rptMsg("".$data . ""); + } + } + else { + #::rptMsg($key_path.$excel." has no values."); + } + } + else { + #::rptMsg($key_path.$excel." not found."); + } + #::rptMsg(""); + # Attempt to retrieve PowerPoint docs + my $ppt = 'PowerPoint\\Recent File List'; + if (my $ppt_key = $of_key->get_subkey($ppt)) { + #::rptMsg($key_path."\\".$ppt); + #::rptMsg("LastWrite Time ".gmtime($ppt_key->get_timestamp())." (UTC)"); + my @vals = $ppt_key->get_list_of_values(); + if (scalar(@vals) > 0) { + my %files; + # Retrieve values and load into a hash for sorting + foreach my $v (@vals) { + my $val = $v->get_name(); + my $data = $v->get_data(); + my $tag = (split(/File/,$val))[1]; + $files{$tag} = $val.":".$data; + } + # Print sorted content to report file + foreach my $u (sort {$a <=> $b} keys %files) { + my ($val,$data) = split(/:/,$files{$u},2); + ::rptMsg("".$data . ""); + } + } + else { + #::rptMsg($key_path."\\".$ppt." has no values."); + } + } + else { + #::rptMsg($key_path."\\".$ppt." not found."); + } + } + else { + #::rptMsg("Could not access ".$key_path); + #::logMsg("Could not access ".$key_path); + } + ::rptMsg(""); + } + else { + #::logMsg("MSOffice version not found."); + #::rptMsg("MSOffice version not found."); + } + } ::rptMsg(""); } diff --git a/thirdparty/rr/plugins/officedocs2010.pl b/thirdparty/rr/plugins/officedocs2010.pl index 2783dc01f6..15073deed3 100644 --- a/thirdparty/rr/plugins/officedocs2010.pl +++ b/thirdparty/rr/plugins/officedocs2010.pl @@ -72,150 +72,151 @@ sub pluginmain { #::logMsg("Launching officedocs2010 v.".$VERSION); #::rptMsg("officedocs2010 v.".$VERSION); # 20110830 [fpi] + banner #::rptMsg("(".getHive().") ".getShortDescr()."\n"); # 20110830 [fpi] + banner - - my $reg = Parse::Win32Registry->new($ntuser); - my $root_key = $reg->get_root_key; - # ::rptMsg("officedocs v.".$VERSION); # 20110830 [fpi] - redundant - my $tag = 0; - my $key_path = "Software\\Microsoft\\Office\\14.0"; - if (defined($root_key->get_subkey($key_path))) { - $tag = 1; - } - - if ($tag) { - #::rptMsg("MSOffice version 2010 located."); - my $key_path = "Software\\Microsoft\\Office\\14.0"; - my $of_key = $root_key->get_subkey($key_path); - if ($of_key) { -# Attempt to retrieve Word docs - my $word = 'Word\\File MRU'; - if (my $word_key = $of_key->get_subkey($word)) { - #::rptMsg($key_path."\\".$word); - #::rptMsg("LastWrite Time ".gmtime($word_key->get_timestamp())." (UTC)"); - my @vals = $word_key->get_list_of_values(); - if (scalar(@vals) > 0) { - my %files; -# Retrieve values and load into a hash for sorting - foreach my $v (@vals) { - my $val = $v->get_name(); - if ($val eq "Max Display") { next; } - my $data = getWinTS($v->get_data()); - my $tag = (split(/Item/,$val))[1]; - $files{$tag} = $val.":".$data; - } -# Print sorted content to report file - foreach my $u (sort {$a <=> $b} keys %files) { - my ($val,$data) = split(/:/,$files{$u},2); - ::rptMsg("".$data . ""); - } - } - else { - #::rptMsg($key_path.$word." has no values."); - } - } - else { - #::rptMsg($key_path.$word." not found."); - } - #::rptMsg(""); -# Attempt to retrieve Excel docs - my $excel = 'Excel\\File MRU'; - if (my $excel_key = $of_key->get_subkey($excel)) { - #::rptMsg($key_path."\\".$excel); - #::rptMsg("LastWrite Time ".gmtime($excel_key->get_timestamp())." (UTC)"); - my @vals = $excel_key->get_list_of_values(); - if (scalar(@vals) > 0) { - my %files; -# Retrieve values and load into a hash for sorting - foreach my $v (@vals) { - my $val = $v->get_name(); - if ($val eq "Max Display") { next; } - my $data = getWinTS($v->get_data()); - my $tag = (split(/Item/,$val))[1]; - $files{$tag} = $val.":".$data; - } -# Print sorted content to report file - foreach my $u (sort {$a <=> $b} keys %files) { - my ($val,$data) = split(/:/,$files{$u},2); - ::rptMsg("".$data . ""); - } - } - else { - #::rptMsg($key_path.$excel." has no values."); - } - } - else { - #::rptMsg($key_path.$excel." not found."); - } - #::rptMsg(""); -# Attempt to retrieve Access docs - my $access = 'Access\\File MRU'; - if (my $access_key = $of_key->get_subkey($access)) { - #::rptMsg($key_path."\\".$access); - #::rptMsg("LastWrite Time ".gmtime($access_key->get_timestamp())." (UTC)"); - my @vals = $access_key->get_list_of_values(); - if (scalar(@vals) > 0) { - my %files; -# Retrieve values and load into a hash for sorting - foreach my $v (@vals) { - my $val = $v->get_name(); - if ($val eq "Max Display") { next; } - my $data = getWinTS($v->get_data()); - my $tag = (split(/Item/,$val))[1]; - $files{$tag} = $val.":".$data; - } -# Print sorted content to report file - foreach my $u (sort {$a <=> $b} keys %files) { - my ($val,$data) = split(/:/,$files{$u},2); - ::rptMsg("".$data . ""); - } - } - else { - # ::rptMsg($key_path.$access." has no values."); - } - } - else { - # ::rptMsg($key_path.$access." not found."); - } - #::rptMsg(""); -# Attempt to retrieve PowerPoint docs - my $ppt = 'PowerPoint\\File MRU'; - if (my $ppt_key = $of_key->get_subkey($ppt)) { - #::rptMsg($key_path."\\".$ppt); - #::rptMsg("LastWrite Time ".gmtime($ppt_key->get_timestamp())." (UTC)"); - my @vals = $ppt_key->get_list_of_values(); - if (scalar(@vals) > 0) { - my %files; -# Retrieve values and load into a hash for sorting - foreach my $v (@vals) { - my $val = $v->get_name(); - if ($val eq "Max Display") { next; } - my $data = getWinTS($v->get_data()); - my $tag = (split(/Item/,$val))[1]; - $files{$tag} = $val.":".$data; - } -# Print sorted content to report file - foreach my $u (sort {$a <=> $b} keys %files) { - my ($val,$data) = split(/:/,$files{$u},2); - ::rptMsg("".$data . ""); - } - } - else { - # ::rptMsg($key_path."\\".$ppt." has no values."); - } - } - else { - # ::rptMsg($key_path."\\".$ppt." not found."); - } - } - else { - # ::rptMsg("Could not access ".$key_path); - # ::logMsg("Could not access ".$key_path); - } - } - else { - # ::logMsg("MSOffice version not found."); - # ::rptMsg("MSOffice version not found."); - } + if (defined(Parse::Win32Registry->new($ntuser))) { + my $reg = Parse::Win32Registry->new($ntuser); + my $root_key = $reg->get_root_key; + # ::rptMsg("officedocs v.".$VERSION); # 20110830 [fpi] - redundant + my $tag = 0; + my $key_path = "Software\\Microsoft\\Office\\14.0"; + if (defined($root_key->get_subkey($key_path))) { + $tag = 1; + } + + if ($tag) { + #::rptMsg("MSOffice version 2010 located."); + my $key_path = "Software\\Microsoft\\Office\\14.0"; + my $of_key = $root_key->get_subkey($key_path); + if ($of_key) { + # Attempt to retrieve Word docs + my $word = 'Word\\File MRU'; + if (my $word_key = $of_key->get_subkey($word)) { + #::rptMsg($key_path."\\".$word); + #::rptMsg("LastWrite Time ".gmtime($word_key->get_timestamp())." (UTC)"); + my @vals = $word_key->get_list_of_values(); + if (scalar(@vals) > 0) { + my %files; + # Retrieve values and load into a hash for sorting + foreach my $v (@vals) { + my $val = $v->get_name(); + if ($val eq "Max Display") { next; } + my $data = getWinTS($v->get_data()); + my $tag = (split(/Item/,$val))[1]; + $files{$tag} = $val.":".$data; + } + # Print sorted content to report file + foreach my $u (sort {$a <=> $b} keys %files) { + my ($val,$data) = split(/:/,$files{$u},2); + ::rptMsg("".$data . ""); + } + } + else { + #::rptMsg($key_path.$word." has no values."); + } + } + else { + #::rptMsg($key_path.$word." not found."); + } + #::rptMsg(""); + # Attempt to retrieve Excel docs + my $excel = 'Excel\\File MRU'; + if (my $excel_key = $of_key->get_subkey($excel)) { + #::rptMsg($key_path."\\".$excel); + #::rptMsg("LastWrite Time ".gmtime($excel_key->get_timestamp())." (UTC)"); + my @vals = $excel_key->get_list_of_values(); + if (scalar(@vals) > 0) { + my %files; + # Retrieve values and load into a hash for sorting + foreach my $v (@vals) { + my $val = $v->get_name(); + if ($val eq "Max Display") { next; } + my $data = getWinTS($v->get_data()); + my $tag = (split(/Item/,$val))[1]; + $files{$tag} = $val.":".$data; + } + # Print sorted content to report file + foreach my $u (sort {$a <=> $b} keys %files) { + my ($val,$data) = split(/:/,$files{$u},2); + ::rptMsg("".$data . ""); + } + } + else { + #::rptMsg($key_path.$excel." has no values."); + } + } + else { + #::rptMsg($key_path.$excel." not found."); + } + #::rptMsg(""); + # Attempt to retrieve Access docs + my $access = 'Access\\File MRU'; + if (my $access_key = $of_key->get_subkey($access)) { + #::rptMsg($key_path."\\".$access); + #::rptMsg("LastWrite Time ".gmtime($access_key->get_timestamp())." (UTC)"); + my @vals = $access_key->get_list_of_values(); + if (scalar(@vals) > 0) { + my %files; + # Retrieve values and load into a hash for sorting + foreach my $v (@vals) { + my $val = $v->get_name(); + if ($val eq "Max Display") { next; } + my $data = getWinTS($v->get_data()); + my $tag = (split(/Item/,$val))[1]; + $files{$tag} = $val.":".$data; + } + # Print sorted content to report file + foreach my $u (sort {$a <=> $b} keys %files) { + my ($val,$data) = split(/:/,$files{$u},2); + ::rptMsg("".$data . ""); + } + } + else { + # ::rptMsg($key_path.$access." has no values."); + } + } + else { + # ::rptMsg($key_path.$access." not found."); + } + #::rptMsg(""); + # Attempt to retrieve PowerPoint docs + my $ppt = 'PowerPoint\\File MRU'; + if (my $ppt_key = $of_key->get_subkey($ppt)) { + #::rptMsg($key_path."\\".$ppt); + #::rptMsg("LastWrite Time ".gmtime($ppt_key->get_timestamp())." (UTC)"); + my @vals = $ppt_key->get_list_of_values(); + if (scalar(@vals) > 0) { + my %files; + # Retrieve values and load into a hash for sorting + foreach my $v (@vals) { + my $val = $v->get_name(); + if ($val eq "Max Display") { next; } + my $data = getWinTS($v->get_data()); + my $tag = (split(/Item/,$val))[1]; + $files{$tag} = $val.":".$data; + } + # Print sorted content to report file + foreach my $u (sort {$a <=> $b} keys %files) { + my ($val,$data) = split(/:/,$files{$u},2); + ::rptMsg("".$data . ""); + } + } + else { + # ::rptMsg($key_path."\\".$ppt." has no values."); + } + } + else { + # ::rptMsg($key_path."\\".$ppt." not found."); + } + } + else { + # ::rptMsg("Could not access ".$key_path); + # ::logMsg("Could not access ".$key_path); + } + } + else { + # ::logMsg("MSOffice version not found."); + # ::rptMsg("MSOffice version not found."); + } + } } 1; diff --git a/thunderbirdparser/nbproject/project.xml b/thunderbirdparser/nbproject/project.xml index 29782066ac..106d3d64a3 100644 --- a/thunderbirdparser/nbproject/project.xml +++ b/thunderbirdparser/nbproject/project.xml @@ -6,6 +6,15 @@ org.sleuthkit.autopsy.thunderbirdparser + + org.netbeans.api.progress + + + + 1 + 1.47.1 + + org.openide.util @@ -36,7 +45,7 @@ 10 - 10.19 + 10.20 diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/MimeJ4MessageParser.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/MimeJ4MessageParser.java index 229615b7b3..6a95e8729a 100755 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/MimeJ4MessageParser.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/MimeJ4MessageParser.java @@ -21,13 +21,11 @@ package org.sleuthkit.autopsy.thunderbirdparser; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; -import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.logging.Level; -import org.apache.james.mime4j.dom.BinaryBody; import org.apache.james.mime4j.dom.Body; import org.apache.james.mime4j.dom.Entity; import org.apache.james.mime4j.dom.Message; @@ -227,15 +225,15 @@ class MimeJ4MessageParser { } else if (e.getDispositionType() != null && e.getDispositionType().equals(ContentDispositionField.DISPOSITION_TYPE_ATTACHMENT)) { handleAttachment(email, e, fileID, index); - } else if (e.getMimeType().equals(HTML_TYPE) - || e.getMimeType().equals(ContentTypeField.TYPE_TEXT_PLAIN)) { + } else if ((e.getMimeType().equals(HTML_TYPE) && (email.getHtmlBody() == null || email.getHtmlBody().isEmpty())) + || (e.getMimeType().equals(ContentTypeField.TYPE_TEXT_PLAIN) && (email.getTextBody() == null || email.getTextBody().isEmpty()))) { handleTextBody(email, (TextBody) e.getBody(), e.getMimeType(), e.getHeader().getFields()); } else { - // Ignore other types. - } + handleAttachment(email, e, fileID, index); + } } } - + /** * Extract text out of a body part of the message. * diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java index e9db7287e8..0f9759819f 100644 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java @@ -54,6 +54,7 @@ import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; import org.sleuthkit.datamodel.DerivedFile; +import org.sleuthkit.datamodel.ReadContentInputStream; import org.sleuthkit.datamodel.Relationship; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; @@ -76,6 +77,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { private Blackboard blackboard; private CommunicationArtifactsHelper communicationArtifactsHelper; + private static final int MBOX_SIZE_TO_SPLIT = 1048576000; private Case currentCase; /** @@ -309,12 +311,75 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { return ProcessResult.OK; } - try { - ContentUtils.writeToFile(abstractFile, file, context::fileIngestIsCancelled); - } catch (IOException ex) { - logger.log(Level.WARNING, "Failed writing mbox file to disk.", ex); //NON-NLS - return ProcessResult.OK; + if (abstractFile.getSize() < MBOX_SIZE_TO_SPLIT) { + + try { + ContentUtils.writeToFile(abstractFile, file, context::fileIngestIsCancelled); + } catch (IOException ex) { + logger.log(Level.WARNING, "Failed writing mbox file to disk.", ex); //NON-NLS + return ProcessResult.OK; + } + + processMboxFile(file, abstractFile, emailFolder); + + if (file.delete() == false) { + logger.log(Level.INFO, "Failed to delete temp file: {0}", file.getName()); //NON-NLS + } + } else { + + List mboxSplitOffsets = new ArrayList<>(); + try{ + mboxSplitOffsets = findMboxSplitOffset(abstractFile, file); + } catch (IOException ex) { + logger.log(Level.WARNING, String.format("Failed finding split offsets for mbox file {0}.", fileName), ex); //NON-NLS + return ProcessResult.OK; + } + + long startingOffset = 0; + for (Long mboxSplitOffset : mboxSplitOffsets) { + File splitFile = new File(fileName + "-" + mboxSplitOffset); + try { + ContentUtils.writeToFile(abstractFile, splitFile, context::fileIngestIsCancelled, startingOffset, mboxSplitOffset); + } catch (IOException ex) { + logger.log(Level.WARNING, "Failed writing split mbox file to disk.", ex); //NON-NLS + return ProcessResult.OK; + } + processMboxFile(splitFile, abstractFile, emailFolder); + startingOffset = mboxSplitOffset; + if (splitFile.delete() == false) { + logger.log(Level.INFO, "Failed to delete temp file: {0}", splitFile); //NON-NLS + } + + } + } + + return ProcessResult.OK; + } + + private List findMboxSplitOffset(AbstractFile abstractFile, File file) throws IOException { + + List mboxSplitOffset = new ArrayList<>(); + + byte[] buffer = new byte[7]; + ReadContentInputStream in = new ReadContentInputStream(abstractFile); + in.skip(MBOX_SIZE_TO_SPLIT); + int len = in.read(buffer); + while (len != -1) { + len = in.read(buffer); + if (buffer[0] == 13 && buffer[1] == 10 && buffer[2] == 70 && buffer[3] == 114 && + buffer[4] == 111 && buffer[5] == 109 && buffer[6] == 32) { + mboxSplitOffset.add(in.getCurPosition() - 5 ); + in.skip(MBOX_SIZE_TO_SPLIT); + } } + + return mboxSplitOffset; + + } + + + private void processMboxFile(File file, AbstractFile abstractFile, String emailFolder) { + MboxParser emailIterator = MboxParser.getEmailIterator( emailFolder, file, abstractFile.getId()); List emails = new ArrayList<>(); @@ -325,7 +390,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { emails.add(emailMessage); } } - + String errors = emailIterator.getErrors(); if (!errors.isEmpty()) { postErrorMessage( @@ -335,11 +400,6 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { } processEmails(emails, MboxParser.getEmailIterator( emailFolder, file, abstractFile.getId()), abstractFile); - if (file.delete() == false) { - logger.log(Level.INFO, "Failed to delete temp file: {0}", file.getName()); //NON-NLS - } - - return ProcessResult.OK; } /** @@ -755,4 +815,5 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { public void shutDown() { // nothing to shut down } + } diff --git a/unix_setup.sh b/unix_setup.sh index edd6c36632..a9d01739f6 100644 --- a/unix_setup.sh +++ b/unix_setup.sh @@ -5,7 +5,7 @@ # NOTE: update_sleuthkit_version.pl updates this value and relies # on it keeping the same name and whitespace. Don't change it. -TSK_VERSION=4.9.0 +TSK_VERSION=4.10.0 # In the beginning... @@ -72,6 +72,10 @@ else echo "done" fi +# make sure thirdparty files are executable +chmod u+x autopsy/markmckinnon/Export* +chmod u+x autopsy/markmckinnon/parse* + # make sure it is executable chmod u+x bin/autopsy