diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java index 62909a7c23..cb6b034da4 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java @@ -2027,6 +2027,43 @@ public class Case { throw ex; } } + + /** + * Create an empty portable case from the current case + * + * @param caseName Case name + * @param portableCaseFolder Case folder - must not exist + * + * @return The portable case database + * + * @throws TskCoreException + */ + public SleuthkitCase createPortableCase(String caseName, File portableCaseFolder) throws TskCoreException { + + if (portableCaseFolder.exists()) { + throw new TskCoreException("Portable case folder " + portableCaseFolder.toString() + " already exists"); + } + if (! portableCaseFolder.mkdirs()) { + throw new TskCoreException("Error creating portable case folder " + portableCaseFolder.toString()); + } + + CaseDetails details = new CaseDetails(caseName, getNumber(), getExaminer(), + getExaminerPhone(), getExaminerEmail(), getCaseNotes()); + try { + CaseMetadata portableCaseMetadata = new CaseMetadata(Case.CaseType.SINGLE_USER_CASE, portableCaseFolder.toString(), + caseName, details, metadata); + portableCaseMetadata.setCaseDatabaseName(SINGLE_USER_CASE_DB_NAME); + } catch (CaseMetadataException ex) { + throw new TskCoreException("Error creating case metadata", ex); + } + + // Create the Sleuthkit case + SleuthkitCase portableSleuthkitCase; + String dbFilePath = Paths.get(portableCaseFolder.toString(), SINGLE_USER_CASE_DB_NAME).toString(); + portableSleuthkitCase = SleuthkitCase.newCase(dbFilePath); + + return portableSleuthkitCase; + } /** * Checks current thread for an interrupt. Usage: checking for user diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseDetails.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseDetails.java index 9182e5706c..b06f455434 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseDetails.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseDetails.java @@ -65,7 +65,7 @@ public final class CaseDetails implements Serializable { examinerPhone = exPhone; examinerEmail = exEmail; caseNotes = notes; - } + } /** * Get the case display name @@ -115,7 +115,7 @@ public final class CaseDetails implements Serializable { /** * Get the case notes * - * @return notes - the note asssociated with the case + * @return notes - the note associated with the case */ public String getCaseNotes() { return caseNotes; diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java index 202ed86f72..9b71d51b06 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java @@ -95,13 +95,19 @@ public final class CaseMetadata { private final static String EXAMINER_ELEMENT_EMAIL = "ExaminerEmail"; //NON-NLS private final static String CASE_ELEMENT_NOTES = "CaseNotes"; //NON-NLS + /* + * Fields from schema version 5 + */ + private static final String SCHEMA_VERSION_FIVE = "5.0"; + private final static String ORIGINAL_CASE_ELEMENT_NAME = "OriginalCase"; //NON-NLS + /* * Unread fields, regenerated on save. */ private final static String MODIFIED_DATE_ELEMENT_NAME = "ModifiedDate"; //NON-NLS private final static String AUTOPSY_SAVED_BY_ELEMENT_NAME = "SavedByAutopsyVersion"; //NON-NLS - private final static String CURRENT_SCHEMA_VERSION = SCHEMA_VERSION_FOUR; + private final static String CURRENT_SCHEMA_VERSION = SCHEMA_VERSION_FIVE; private final Path metadataFilePath; private Case.CaseType caseType; @@ -112,6 +118,7 @@ public final class CaseMetadata { private String textIndexName; // Legacy private String createdDate; private String createdByVersion; + private CaseMetadata originalMetadata = null; // For portable cases /** * Gets the file extension used for case metadata files. @@ -130,21 +137,33 @@ public final class CaseMetadata { public static DateFormat getDateFormat() { return new SimpleDateFormat(DATE_FORMAT_STRING, Locale.US); } + + /** + * Constructs a CaseMetadata object for a new case. The metadata is not + * persisted to the case metadata file until writeFile or a setX method is + * called. + * + * @param caseType The type of case. + * @param caseDirectory The case directory. + * @param caseName The immutable name of the case. + * @param caseDetails The details for the case + */ + CaseMetadata(Case.CaseType caseType, String caseDirectory, String caseName, CaseDetails caseDetails) { + this(caseType, caseDirectory, caseName, caseDetails, null); + } /** * Constructs a CaseMetadata object for a new case. The metadata is not * persisted to the case metadata file until writeFile or a setX method is * called. * - * @param caseDirectory The case directory. * @param caseType The type of case. + * @param caseDirectory The case directory. * @param caseName The immutable name of the case. - * @param caseDisplayName The display name of the case, can be changed by a - * user. - * @param caseNumber The case number. - * @param examiner The name of the case examiner. + * @param caseDetails The details for the case + * @param originalMetadata The metadata object from the original case */ - CaseMetadata(Case.CaseType caseType, String caseDirectory, String caseName, CaseDetails caseDetails) { + CaseMetadata(Case.CaseType caseType, String caseDirectory, String caseName, CaseDetails caseDetails, CaseMetadata originalMetadata) { metadataFilePath = Paths.get(caseDirectory, caseDetails.getCaseDisplayName() + FILE_EXTENSION); this.caseType = caseType; this.caseName = caseName; @@ -154,6 +173,7 @@ public final class CaseMetadata { textIndexName = ""; createdByVersion = Version.getVersion(); createdDate = CaseMetadata.DATE_FORMAT.format(new Date()); + this.originalMetadata = originalMetadata; } /** @@ -417,17 +437,42 @@ public final class CaseMetadata { /* * Create the children of the case element. */ - createChildElement(doc, caseElement, CASE_NAME_ELEMENT_NAME, caseName); - createChildElement(doc, caseElement, CASE_DISPLAY_NAME_ELEMENT_NAME, caseDetails.getCaseDisplayName()); - createChildElement(doc, caseElement, CASE_NUMBER_ELEMENT_NAME, caseDetails.getCaseNumber()); - createChildElement(doc, caseElement, EXAMINER_ELEMENT_NAME, caseDetails.getExaminerName()); - createChildElement(doc, caseElement, EXAMINER_ELEMENT_PHONE, caseDetails.getExaminerPhone()); - createChildElement(doc, caseElement, EXAMINER_ELEMENT_EMAIL, caseDetails.getExaminerEmail()); - createChildElement(doc, caseElement, CASE_ELEMENT_NOTES, caseDetails.getCaseNotes()); - createChildElement(doc, caseElement, CASE_TYPE_ELEMENT_NAME, caseType.toString()); - createChildElement(doc, caseElement, CASE_DB_ABSOLUTE_PATH_ELEMENT_NAME, caseDatabasePath); - createChildElement(doc, caseElement, CASE_DB_NAME_RELATIVE_ELEMENT_NAME, caseDatabaseName); - createChildElement(doc, caseElement, TEXT_INDEX_ELEMENT, textIndexName); + createCaseElements(doc, caseElement, this); + + /* + * Add original case element + */ + Element originalCaseElement = doc.createElement(ORIGINAL_CASE_ELEMENT_NAME); + rootElement.appendChild(originalCaseElement); + if (originalMetadata != null) { + createChildElement(doc, originalCaseElement, CREATED_DATE_ELEMENT_NAME, originalMetadata.createdDate); + Element originalCaseDetailsElement = doc.createElement(CASE_ELEMENT_NAME); + originalCaseElement.appendChild(originalCaseDetailsElement); + createCaseElements(doc, originalCaseDetailsElement, originalMetadata); + } + + } + + /** + * Write the case element children for the given metadata object + * + * @param doc The document. + * @param caseElement The case element parent + * @param metadataToWrite The CaseMetadata object to read from + */ + private void createCaseElements(Document doc, Element caseElement, CaseMetadata metadataToWrite) { + CaseDetails caseDetailsToWrite = metadataToWrite.caseDetails; + createChildElement(doc, caseElement, CASE_NAME_ELEMENT_NAME, metadataToWrite.caseName); + createChildElement(doc, caseElement, CASE_DISPLAY_NAME_ELEMENT_NAME, caseDetailsToWrite.getCaseDisplayName()); + createChildElement(doc, caseElement, CASE_NUMBER_ELEMENT_NAME, caseDetailsToWrite.getCaseNumber()); + createChildElement(doc, caseElement, EXAMINER_ELEMENT_NAME, caseDetailsToWrite.getExaminerName()); + createChildElement(doc, caseElement, EXAMINER_ELEMENT_PHONE, caseDetailsToWrite.getExaminerPhone()); + createChildElement(doc, caseElement, EXAMINER_ELEMENT_EMAIL, caseDetailsToWrite.getExaminerEmail()); + createChildElement(doc, caseElement, CASE_ELEMENT_NOTES, caseDetailsToWrite.getCaseNotes()); + createChildElement(doc, caseElement, CASE_TYPE_ELEMENT_NAME, metadataToWrite.caseType.toString()); + createChildElement(doc, caseElement, CASE_DB_ABSOLUTE_PATH_ELEMENT_NAME, metadataToWrite.caseDatabasePath); + createChildElement(doc, caseElement, CASE_DB_NAME_RELATIVE_ELEMENT_NAME, metadataToWrite.caseDatabaseName); + createChildElement(doc, caseElement, TEXT_INDEX_ELEMENT, metadataToWrite.textIndexName); } /** @@ -505,7 +550,9 @@ public final class CaseMetadata { examinerEmail = getElementTextContent(caseElement, EXAMINER_ELEMENT_EMAIL, false); caseNotes = getElementTextContent(caseElement, CASE_ELEMENT_NOTES, false); } - this.caseDetails = new CaseDetails(caseDisplayName, caseNumber, examinerName, examinerPhone, examinerEmail, caseNotes); + + this.caseDetails = new CaseDetails(caseDisplayName, caseNumber, examinerName, examinerPhone, examinerEmail, + caseNotes); this.caseType = Case.CaseType.fromString(getElementTextContent(caseElement, CASE_TYPE_ELEMENT_NAME, true)); if (null == this.caseType) { throw new CaseMetadataException("Case metadata file corrupted"); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ContentChildren.java b/Core/src/org/sleuthkit/autopsy/datamodel/ContentChildren.java index 3f8dcfdb07..cc03bb4d46 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ContentChildren.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ContentChildren.java @@ -26,6 +26,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.Directory; import org.sleuthkit.datamodel.FileSystem; +import org.sleuthkit.datamodel.LocalDirectory; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.VolumeSystem; @@ -88,6 +89,13 @@ class ContentChildren extends AbstractContentChildren { } else { children.add(c); } + } else if (c instanceof LocalDirectory) { + LocalDirectory localDir = (LocalDirectory)c; + if (localDir.isRoot()) { + children.addAll(getDisplayChildren(localDir)); + } else { + children.add(c); + } } else { children.add(c); } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ViewContextAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ViewContextAction.java index 5cf09429ea..62c57f7118 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ViewContextAction.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ViewContextAction.java @@ -46,6 +46,7 @@ import org.sleuthkit.autopsy.datamodel.DataSourcesNode; import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; import org.sleuthkit.autopsy.datamodel.RootContentChildren; import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentVisitor; import org.sleuthkit.datamodel.DataSource; @@ -242,6 +243,17 @@ public class ViewContextAction extends AbstractAction { */ DisplayableItemNode undecoratedParentNode = (DisplayableItemNode) ((DirectoryTreeFilterNode) parentTreeViewNode).getOriginal(); undecoratedParentNode.setChildNodeSelectionInfo(new ContentNodeSelectionInfo(content)); + if (content instanceof BlackboardArtifact) { + BlackboardArtifact artifact = ((BlackboardArtifact) content); + long associatedId = artifact.getObjectID(); + try { + Content associatedFileContent = artifact.getSleuthkitCase().getContentById(associatedId); + undecoratedParentNode.setChildNodeSelectionInfo(new ContentNodeSelectionInfo(associatedFileContent)); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Could not find associated content from artifact with id %d", artifact.getId()); + } + } + TreeView treeView = treeViewTopComponent.getTree(); treeView.expandNode(parentTreeViewNode); if (treeViewTopComponent.getSelectedNode().equals(parentTreeViewNode)) { diff --git a/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestPipeline.java b/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestPipeline.java index 363af546de..79d39381f1 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestPipeline.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestPipeline.java @@ -109,9 +109,9 @@ final class DataSourceIngestPipeline { this.job.updateDataSourceIngestProgressBarDisplayName(displayName); this.job.switchDataSourceIngestProgressBarToIndeterminate(); DataSourceIngestPipeline.ingestManager.setIngestTaskProgress(task, module.getDisplayName()); - logger.log(Level.INFO, "{0} analysis of {1} (jobId={2}) starting", new Object[]{module.getDisplayName(), this.job.getDataSource().getName(), this.job.getDataSource().getId()}); //NON-NLS + logger.log(Level.INFO, "{0} analysis of {1} (jobId={2}) starting", new Object[]{module.getDisplayName(), this.job.getDataSource().getName(), this.job.getId()}); //NON-NLS module.process(dataSource, new DataSourceIngestModuleProgress(this.job)); - logger.log(Level.INFO, "{0} analysis of {1} (jobId={2}) finished", new Object[]{module.getDisplayName(), this.job.getDataSource().getName(), this.job.getDataSource().getId()}); //NON-NLS + logger.log(Level.INFO, "{0} analysis of {1} (jobId={2}) finished", new Object[]{module.getDisplayName(), this.job.getDataSource().getName(), this.job.getId()}); //NON-NLS } catch (Throwable ex) { // Catch-all exception firewall errors.add(new IngestModuleError(module.getDisplayName(), ex)); String msg = ex.getMessage(); diff --git a/Core/src/org/sleuthkit/autopsy/modules/encryptiondetection/EncryptionDetectionDataSourceIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/encryptiondetection/EncryptionDetectionDataSourceIngestModule.java index 46421475ef..7bf426343d 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/encryptiondetection/EncryptionDetectionDataSourceIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/encryptiondetection/EncryptionDetectionDataSourceIngestModule.java @@ -78,6 +78,12 @@ final class EncryptionDetectionDataSourceIngestModule implements DataSourceInges try { if (dataSource instanceof Image) { + + if (((Image) dataSource).getPaths().length == 0) { + logger.log(Level.SEVERE, String.format("Unable to process data source '%s' - image has no paths", dataSource.getName())); + return IngestModule.ProcessResult.ERROR; + } + List volumeSystems = ((Image) dataSource).getVolumeSystems(); for (VolumeSystem volumeSystem : volumeSystems) { for (Volume volume : volumeSystem.getVolumes()) { diff --git a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties index 599aa1d215..466a645600 100644 --- a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties @@ -252,3 +252,9 @@ ReportHTMLConfigurationPanel.headerTextField.text= ReportHTMLConfigurationPanel.footerTextField.text= ReportHTMLConfigurationPanel.headerLabel.text=Header: ReportHTMLConfigurationPanel.footerLabel.text=Footer: +CreatePortableCasePanel.selectAllButton.text=Select All +CreatePortableCasePanel.deselectAllButton.text=Deselect All +CreatePortableCasePanel.outputFolderTextField.text=jTextField1 +CreatePortableCasePanel.chooseOutputFolderButton.text=Choose folder +CreatePortableCasePanel.jLabel1.text=Export files tagged as: +CreatePortableCasePanel.jLabel2.text=Select output folder: diff --git a/Core/src/org/sleuthkit/autopsy/report/CreatePortableCaseModule.java b/Core/src/org/sleuthkit/autopsy/report/CreatePortableCaseModule.java new file mode 100644 index 0000000000..cb751fa53b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/report/CreatePortableCaseModule.java @@ -0,0 +1,476 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.report; + +import org.openide.util.lookup.ServiceProvider; +import javax.swing.JPanel; +import java.util.logging.Level; +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.coreutils.FileUtil; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.datamodel.ContentUtils; +import org.sleuthkit.autopsy.datamodel.utils.FileTypeUtils.FileTypeCategory; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.FileSystem; +import org.sleuthkit.datamodel.Image; +import org.sleuthkit.datamodel.LocalFilesDataSource; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.SleuthkitCase.CaseDbTransaction; +import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; +import org.sleuthkit.datamodel.Volume; +import org.sleuthkit.datamodel.VolumeSystem; + +/** + * Creates a portable case from tagged files + */ +@ServiceProvider(service = GeneralReportModule.class) +public class CreatePortableCaseModule implements GeneralReportModule { + private static final Logger logger = Logger.getLogger(CreatePortableCaseModule.class.getName()); + private static final String FILE_FOLDER_NAME = "PortableCaseFiles"; + private static final String UNKNOWN_FILE_TYPE_FOLDER = "Other"; + private CreatePortableCasePanel configPanel; + + // These are the types for the exported file subfolders + private static final List FILE_TYPE_CATEGORIES = Arrays.asList(FileTypeCategory.AUDIO, FileTypeCategory.DOCUMENTS, + FileTypeCategory.EXECUTABLE, FileTypeCategory.IMAGE, FileTypeCategory.VIDEO); + + private Case currentCase = null; + private SleuthkitCase skCase = null; + private File caseFolder = null; + private File copiedFilesFolder = null; + + // Maps old object ID from current case to new object in portable case + private final Map oldIdToNewContent = new HashMap<>(); + + // Maps new object ID to the new object + private final Map newIdToContent = new HashMap<>(); + + // Maps old TagName to new TagName + private final Map oldTagNameToNewTagName = new HashMap<>(); + + public CreatePortableCaseModule() { + // Nothing to do here + } + + @NbBundle.Messages({ + "CreatePortableCaseModule.getName.name=Portable Case" + }) + @Override + public String getName() { + return Bundle.CreatePortableCaseModule_getName_name(); + } + + @NbBundle.Messages({ + "CreatePortableCaseModule.getDescription.description=Copies selected tagged items to a new single-user case that will work anywhere" + }) + @Override + public String getDescription() { + return Bundle.CreatePortableCaseModule_getDescription_description(); + } + + @Override + public String getRelativeFilePath() { + return ""; + } + + /** + * Convenience method to avoid code duplication. + * Assumes that if an exception is supplied then the error is SEVERE. Otherwise + * it is logged as a WARNING. + * + * @param logWarning Warning to write to the log + * @param dialogWarning Warning to write to a pop-up window + * @param ex The exception (can be null) + * @param progressPanel The report progress panel + */ + private void handleError(String logWarning, String dialogWarning, Exception ex, ReportProgressPanel progressPanel) { + if (ex == null) { + logger.log(Level.WARNING, logWarning); + } else { + logger.log(Level.SEVERE, logWarning, ex); + } + MessageNotifyUtil.Message.error(dialogWarning); + progressPanel.setIndeterminate(false); + progressPanel.complete(ReportProgressPanel.ReportStatus.ERROR); + cleanup(); + } + + @NbBundle.Messages({ + "CreatePortableCaseModule.generateReport.verifying=Verifying selected parameters...", + "CreatePortableCaseModule.generateReport.creatingCase=Creating portable case database...", + "CreatePortableCaseModule.generateReport.copyingTags=Copying tags...", + "# {0} - tag name", + "CreatePortableCaseModule.generateReport.copyingFiles=Copying files tagged as {0}...", + "# {0} - output folder", + "CreatePortableCaseModule.generateReport.outputDirDoesNotExist=Output folder {0} does not exist", + "# {0} - output folder", + "CreatePortableCaseModule.generateReport.outputDirIsNotDir=Output folder {0} is not a folder", + "CreatePortableCaseModule.generateReport.noTagsSelected=No tags selected for export.", + "CreatePortableCaseModule.generateReport.caseClosed=Current case has been closed", + "CreatePortableCaseModule.generateReport.errorCopyingTags=Error copying tags", + "CreatePortableCaseModule.generateReport.errorCopyingFiles=Error copying tagged files" + }) + @Override + public void generateReport(String reportPath, ReportProgressPanel progressPanel) { + progressPanel.setIndeterminate(true); + progressPanel.start(); + progressPanel.updateStatusLabel(Bundle.CreatePortableCaseModule_generateReport_verifying()); + + // Clear out any old values + cleanup(); + + // Validate the input parameters + File outputDir = new File(configPanel.getOutputFolder()); + if (! outputDir.exists()) { + handleError("Output folder " + outputDir.toString() + " does not exist", + Bundle.CreatePortableCaseModule_generateReport_outputDirDoesNotExist(outputDir.toString()), null, progressPanel); + return; + } + + if (! outputDir.isDirectory()) { + handleError("Output folder " + outputDir.toString() + " is not a folder", + Bundle.CreatePortableCaseModule_generateReport_outputDirIsNotDir(outputDir.toString()), null, progressPanel); + return; + } + + List tagNames = configPanel.getSelectedTagNames(); + if (tagNames.isEmpty()) { + handleError("No tags selected for export", + Bundle.CreatePortableCaseModule_generateReport_noTagsSelected(), null, progressPanel); + return; + } + + // Save the current case object + try { + currentCase = Case.getCurrentCaseThrows(); + } catch (NoCurrentCaseException ex) { + handleError("Current case has been closed", + Bundle.CreatePortableCaseModule_generateReport_caseClosed(), null, progressPanel); + return; + } + + + // Create the case. + // skCase and caseFolder will be set here. + progressPanel.updateStatusLabel(Bundle.CreatePortableCaseModule_generateReport_creatingCase()); + createCase(outputDir, progressPanel); + if (skCase == null) { + // The error has already been handled + return; + } + + // Check for cancellation + if (progressPanel.getStatus() == ReportProgressPanel.ReportStatus.CANCELED) { + cleanup(); + return; + } + + // Copy the selected tags + progressPanel.updateStatusLabel(Bundle.CreatePortableCaseModule_generateReport_copyingTags()); + try { + for(TagName tagName:tagNames) { + TagName newTagName = skCase.addOrUpdateTagName(tagName.getDisplayName(), tagName.getDescription(), tagName.getColor(), tagName.getKnownStatus()); + oldTagNameToNewTagName.put(tagName, newTagName); + } + } catch (TskCoreException ex) { + handleError("Error copying tags", Bundle.CreatePortableCaseModule_generateReport_errorCopyingTags(), ex, progressPanel); + return; + } + + // Copy the tagged files + try { + for(TagName tagName:tagNames) { + // Check for cancellation + if (progressPanel.getStatus() == ReportProgressPanel.ReportStatus.CANCELED) { + return; + } + progressPanel.updateStatusLabel(Bundle.CreatePortableCaseModule_generateReport_copyingFiles(tagName.getDisplayName())); + addFilesToPortableCase(tagName, progressPanel); + } + } catch (TskCoreException ex) { + handleError("Error copying tagged files", Bundle.CreatePortableCaseModule_generateReport_errorCopyingFiles(), ex, progressPanel); + return; + } + + // Close the case connections and clear out the maps + cleanup(); + + progressPanel.complete(ReportProgressPanel.ReportStatus.COMPLETE); + + } + + /** + * Create the case directory and case database. + * skCase will be set if this completes without error. + * + * @param outputDir The parent for the case folder + * @param progressPanel + */ + @NbBundle.Messages({ + "# {0} - case folder", + "CreatePortableCaseModule.createCase.caseDirExists=Case folder {0} already exists", + "CreatePortableCaseModule.createCase.errorCreatingCase=Error creating case", + "# {0} - folder", + "CreatePortableCaseModule.createCase.errorCreatingFolder=Error creating folder {0}", + }) + private void createCase(File outputDir, ReportProgressPanel progressPanel) { + + // Create the case folder + String caseName = currentCase.getDisplayName() + " (Portable)"; + caseFolder = Paths.get(outputDir.toString(), caseName).toFile(); + + if (caseFolder.exists()) { + handleError("Case folder " + caseFolder.toString() + " already exists", + Bundle.CreatePortableCaseModule_createCase_caseDirExists(caseFolder.toString()), null, progressPanel); + return; + } + + // Create the case + try { + skCase = currentCase.createPortableCase(caseName, caseFolder); + } catch (TskCoreException ex) { + handleError("Error creating case " + caseName + " in folder " + caseFolder.toString(), + Bundle.CreatePortableCaseModule_createCase_errorCreatingCase(), ex, progressPanel); + return; + } + + // Create the base folder for the copied files + copiedFilesFolder = Paths.get(caseFolder.toString(), FILE_FOLDER_NAME).toFile(); + if (! copiedFilesFolder.mkdir()) { + handleError("Error creating folder " + copiedFilesFolder.toString(), + Bundle.CreatePortableCaseModule_createCase_errorCreatingFolder(copiedFilesFolder.toString()), null, progressPanel); + return; + } + + // Create subfolders for the copied files + for (FileTypeCategory cat:FILE_TYPE_CATEGORIES) { + File subFolder = Paths.get(copiedFilesFolder.toString(), cat.getDisplayName()).toFile(); + if (! subFolder.mkdir()) { + handleError("Error creating folder " + subFolder.toString(), + Bundle.CreatePortableCaseModule_createCase_errorCreatingFolder(subFolder.toString()), null, progressPanel); + return; + } + } + File unknownTypeFolder = Paths.get(copiedFilesFolder.toString(), UNKNOWN_FILE_TYPE_FOLDER).toFile(); + if (! unknownTypeFolder.mkdir()) { + handleError("Error creating folder " + unknownTypeFolder.toString(), + Bundle.CreatePortableCaseModule_createCase_errorCreatingFolder(unknownTypeFolder.toString()), null, progressPanel); + return; + } + + } + + /** + * Add all files with a given tag to the portable case. + * + * @param oldTagName + * @param progressPanel + * @throws TskCoreException + */ + @NbBundle.Messages({ + "# {0} - File name", + "CreatePortableCaseModule.addFilesToPortableCase.copyingFile=Copying file {0}", + }) + private void addFilesToPortableCase(TagName oldTagName, ReportProgressPanel progressPanel) throws TskCoreException { + + // Get all the tags in the current case + List tags = currentCase.getServices().getTagsManager().getContentTagsByTagName(oldTagName); + + // Copy the files into the portable case and tag + for (ContentTag tag : tags) { + + // Check for cancellation + if (progressPanel.getStatus() == ReportProgressPanel.ReportStatus.CANCELED) { + return; + } + + Content content = tag.getContent(); + if (content instanceof AbstractFile) { + AbstractFile file = (AbstractFile) content; + String filePath = file.getParentPath() + file.getName(); + progressPanel.updateStatusLabel(Bundle.CreatePortableCaseModule_addFilesToPortableCase_copyingFile(filePath)); + + long newFileId; + CaseDbTransaction trans = skCase.beginTransaction(); + try { + newFileId = copyContent(file, trans); + trans.commit(); + } catch (TskCoreException ex) { + trans.rollback(); + throw(ex); + } + + // Tag the file + if (! oldTagNameToNewTagName.containsKey(tag.getName())) { + throw new TskCoreException("TagName map is missing entry for ID " + tag.getName().getId() + " with display name " + tag.getName().getDisplayName()); + } + skCase.addContentTag(newIdToContent.get(newFileId), oldTagNameToNewTagName.get(tag.getName()), tag.getComment(), tag.getBeginByteOffset(), tag.getEndByteOffset()); + } + } + } + + /** + * Returns the object ID for the given content object in the portable case. + * + * @param content The content object to copy into the portable case + * @param trans The current transaction + * + * @return the new object ID for this content + * + * @throws TskCoreException + */ + private long copyContent(Content content, CaseDbTransaction trans) throws TskCoreException { + + // Check if we've already copied this content + if (oldIdToNewContent.containsKey(content.getId())) { + return oldIdToNewContent.get(content.getId()).getId(); + } + + // Otherwise: + // - Make parent of this object (if applicable) + // - Copy this content + long parentId = 0; + if (content.getParent() != null) { + parentId = copyContent(content.getParent(), trans); + } + + Content newContent; + if (content instanceof Image) { + Image image = (Image)content; + newContent = skCase.addImage(image.getType(), image.getSsize(), image.getSize(), image.getName(), + new ArrayList<>(), image.getTimeZone(), image.getMd5(), image.getSha1(), image.getSha256(), image.getDeviceId(), trans); + } else if (content instanceof VolumeSystem) { + VolumeSystem vs = (VolumeSystem)content; + newContent = skCase.addVolumeSystem(parentId, vs.getType(), vs.getOffset(), vs.getBlockSize(), trans); + } else if (content instanceof Volume) { + Volume vs = (Volume)content; + newContent = skCase.addVolume(parentId, vs.getAddr(), vs.getStart(), vs.getLength(), + vs.getDescription(), vs.getFlags(), trans); + } else if (content instanceof FileSystem) { + FileSystem fs = (FileSystem)content; + newContent = skCase.addFileSystem(parentId, fs.getImageOffset(), fs.getFsType(), fs.getBlock_size(), + fs.getBlock_count(), fs.getRoot_inum(), fs.getFirst_inum(), fs.getLastInum(), + fs.getName(), trans); + } else if (content instanceof AbstractFile) { + AbstractFile abstractFile = (AbstractFile)content; + + if (abstractFile instanceof LocalFilesDataSource) { + LocalFilesDataSource localFilesDS = (LocalFilesDataSource)abstractFile; + newContent = skCase.addLocalFilesDataSource(localFilesDS.getDeviceId(), localFilesDS.getName(), localFilesDS.getTimeZone(), trans); + } else { + if (abstractFile.isDir()) { + newContent = skCase.addLocalDirectory(parentId, abstractFile.getName(), trans); + } else { + try { + // Copy the file + String fileName = abstractFile.getId() + "-" + FileUtil.escapeFileName(abstractFile.getName()); + String exportSubFolder = getExportSubfolder(abstractFile); + File exportFolder = Paths.get(copiedFilesFolder.toString(), exportSubFolder).toFile(); + File localFile = new File(exportFolder, fileName); + ContentUtils.writeToFile(abstractFile, localFile); + + // Get the new parent object in the portable case database + Content oldParent = abstractFile.getParent(); + if (! oldIdToNewContent.containsKey(oldParent.getId())) { + throw new TskCoreException("Parent of file with ID " + abstractFile.getId() + " has not been created"); + } + Content newParent = oldIdToNewContent.get(oldParent.getId()); + + // Construct the relative path to the copied file + String relativePath = FILE_FOLDER_NAME + File.separator + exportSubFolder + File.separator + fileName; + + newContent = skCase.addLocalFile(abstractFile.getName(), relativePath, abstractFile.getSize(), + abstractFile.getCtime(), abstractFile.getCrtime(), abstractFile.getAtime(), abstractFile.getMtime(), + abstractFile.getMd5Hash(), abstractFile.getKnown(), abstractFile.getMIMEType(), + true, TskData.EncodingType.NONE, + newParent, trans); + } catch (IOException ex) { + throw new TskCoreException("Error copying file " + abstractFile.getName() + " with original obj ID " + + abstractFile.getId(), ex); + } + } + } + } else { + throw new TskCoreException("Trying to copy unexpected Content type " + content.getClass().getName()); + } + + // Save the new object + oldIdToNewContent.put(content.getId(), newContent); + newIdToContent.put(newContent.getId(), newContent); + return oldIdToNewContent.get(content.getId()).getId(); + } + + /** + * Return the subfolder name for this file based on MIME type + * + * @param abstractFile the file + * + * @return the name of the appropriate subfolder for this file type + */ + private String getExportSubfolder(AbstractFile abstractFile) { + if (abstractFile.getMIMEType() == null || abstractFile.getMIMEType().isEmpty()) { + return UNKNOWN_FILE_TYPE_FOLDER; + } + + for (FileTypeCategory cat:FILE_TYPE_CATEGORIES) { + if (cat.getMediaTypes().contains(abstractFile.getMIMEType())) { + return cat.getDisplayName(); + } + } + return UNKNOWN_FILE_TYPE_FOLDER; + } + + /** + * Clear out the maps and other fields and close the database connections. + */ + private void cleanup() { + oldIdToNewContent.clear(); + newIdToContent.clear(); + oldTagNameToNewTagName.clear(); + currentCase = null; + if (skCase != null) { + // We want to close the database connections here but it is currently not possible. JIRA-4736 + skCase = null; + } + caseFolder = null; + copiedFilesFolder = null; + } + + + @Override + public JPanel getConfigurationPanel() { + configPanel = new CreatePortableCasePanel(); + return configPanel; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/report/CreatePortableCasePanel.form b/Core/src/org/sleuthkit/autopsy/report/CreatePortableCasePanel.form new file mode 100644 index 0000000000..a5c2b30340 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/report/CreatePortableCasePanel.form @@ -0,0 +1,159 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/report/CreatePortableCasePanel.java b/Core/src/org/sleuthkit/autopsy/report/CreatePortableCasePanel.java new file mode 100644 index 0000000000..13fd3373db --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/report/CreatePortableCasePanel.java @@ -0,0 +1,329 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.report; + +import java.awt.Component; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.File; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import javax.swing.JCheckBox; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.ListCellRenderer; +import javax.swing.ListModel; +import javax.swing.event.ListDataListener; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * + */ +@NbBundle.Messages({ + "CreatePortableCasePanel.error.errorTitle=Error getting tag names for case", + "CreatePortableCasePanel.error.noOpenCase=There is no case open", + "CreatePortableCasePanel.error.errorLoadingTags=Error loading tags", +}) +@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives +class CreatePortableCasePanel extends javax.swing.JPanel { + + private static final long serialVersionUID = 1L; + private List tagNames; + private final Map tagNameSelections = new LinkedHashMap<>(); + private final TagNamesListModel tagsNamesListModel = new TagNamesListModel(); + private final TagsNamesListCellRenderer tagsNamesRenderer = new TagsNamesListCellRenderer(); + + /** + * Creates new form CreatePortableCasePanel + */ + public CreatePortableCasePanel() { + initComponents(); + customizeComponents(); + } + + private void customizeComponents() { + populateTagNameComponents(); + + try { + outputFolderTextField.setText(Case.getCurrentCaseThrows().getReportDirectory()); + } catch (NoCurrentCaseException ex) { + Logger.getLogger(CreatePortableCasePanel.class.getName()).log(Level.SEVERE, "Exception while getting open case.", ex); + JOptionPane.showMessageDialog(this, Bundle.CreatePortableCasePanel_error_noOpenCase(), Bundle.CreatePortableCasePanel_error_errorTitle(), JOptionPane.ERROR_MESSAGE); + } + } + + private void populateTagNameComponents() { + // Get the tag names in use for the current case. + try { + tagNames = Case.getCurrentCaseThrows().getServices().getTagsManager().getTagNamesInUse(); + } catch (TskCoreException ex) { + Logger.getLogger(CreatePortableCasePanel.class.getName()).log(Level.SEVERE, "Failed to get tag names", ex); + JOptionPane.showMessageDialog(this, Bundle.CreatePortableCasePanel_error_errorLoadingTags(), Bundle.CreatePortableCasePanel_error_errorTitle(), JOptionPane.ERROR_MESSAGE); + } catch (NoCurrentCaseException ex) { + Logger.getLogger(CreatePortableCasePanel.class.getName()).log(Level.SEVERE, "Exception while getting open case.", ex); + JOptionPane.showMessageDialog(this, Bundle.CreatePortableCasePanel_error_noOpenCase(), Bundle.CreatePortableCasePanel_error_errorTitle(), JOptionPane.ERROR_MESSAGE); + } + + // Mark the tag names as unselected. Note that tagNameSelections is a + // LinkedHashMap so that order is preserved and the tagNames and tagNameSelections + // containers are "parallel" containers. + for (TagName tagName : tagNames) { + tagNameSelections.put(tagName.getDisplayName(), Boolean.FALSE); + } + + // Set up the tag names JList component to be a collection of check boxes + // for selecting tag names. The mouse click listener updates tagNameSelections + // to reflect user choices. + tagNamesListBox.setModel(tagsNamesListModel); + tagNamesListBox.setCellRenderer(tagsNamesRenderer); + tagNamesListBox.setVisibleRowCount(-1); + tagNamesListBox.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent evt) { + JList list = (JList) evt.getSource(); + int index = list.locationToIndex(evt.getPoint()); + if (index > -1) { + String value = tagsNamesListModel.getElementAt(index); + tagNameSelections.put(value, !tagNameSelections.get(value)); + list.repaint(); + } + } + }); + } + + /** + * Gets the subset of the tag names in use selected by the user. + * + * @return A list, possibly empty, of TagName data transfer objects (DTOs). + */ + List getSelectedTagNames() { + List selectedTagNames = new ArrayList<>(); + for (TagName tagName : tagNames) { + if (tagNameSelections.get(tagName.getDisplayName())) { + selectedTagNames.add(tagName); + } + } + return selectedTagNames; + } + + /** + * Get the output folder + * + * @return The output folder for the portable case + */ + String getOutputFolder() { + return outputFolderTextField.getText(); + } + + // This class is a list model for the tag names JList component. + private class TagNamesListModel implements ListModel { + + @Override + public int getSize() { + return tagNames.size(); + } + + @Override + public String getElementAt(int index) { + return tagNames.get(index).getDisplayName(); + } + + @Override + public void addListDataListener(ListDataListener l) { + } + + @Override + public void removeListDataListener(ListDataListener l) { + } + } + + // This class renders the items in the tag names JList component as JCheckbox components. + private class TagsNamesListCellRenderer extends JCheckBox implements ListCellRenderer { + private static final long serialVersionUID = 1L; + + @Override + public Component getListCellRendererComponent(JList list, String value, int index, boolean isSelected, boolean cellHasFocus) { + if (value != null) { + setEnabled(list.isEnabled()); + setSelected(tagNameSelections.get(value)); + setFont(list.getFont()); + setBackground(list.getBackground()); + setForeground(list.getForeground()); + setText(value); + return this; + } + return new JLabel(); + } + } + + /** + * 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() { + + selectAllButton = new javax.swing.JButton(); + deselectAllButton = new javax.swing.JButton(); + jScrollPane1 = new javax.swing.JScrollPane(); + tagNamesListBox = new javax.swing.JList<>(); + outputFolderTextField = new javax.swing.JTextField(); + chooseOutputFolderButton = new javax.swing.JButton(); + jLabel1 = new javax.swing.JLabel(); + jLabel2 = new javax.swing.JLabel(); + + org.openide.awt.Mnemonics.setLocalizedText(selectAllButton, org.openide.util.NbBundle.getMessage(CreatePortableCasePanel.class, "CreatePortableCasePanel.selectAllButton.text")); // NOI18N + selectAllButton.setMaximumSize(new java.awt.Dimension(99, 23)); + selectAllButton.setMinimumSize(new java.awt.Dimension(99, 23)); + selectAllButton.setPreferredSize(new java.awt.Dimension(99, 23)); + selectAllButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + selectAllButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(deselectAllButton, org.openide.util.NbBundle.getMessage(CreatePortableCasePanel.class, "CreatePortableCasePanel.deselectAllButton.text")); // NOI18N + deselectAllButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + deselectAllButtonActionPerformed(evt); + } + }); + + jScrollPane1.setViewportView(tagNamesListBox); + + outputFolderTextField.setText(org.openide.util.NbBundle.getMessage(CreatePortableCasePanel.class, "CreatePortableCasePanel.outputFolderTextField.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(chooseOutputFolderButton, org.openide.util.NbBundle.getMessage(CreatePortableCasePanel.class, "CreatePortableCasePanel.chooseOutputFolderButton.text")); // NOI18N + chooseOutputFolderButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chooseOutputFolderButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(CreatePortableCasePanel.class, "CreatePortableCasePanel.jLabel1.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(jLabel2, org.openide.util.NbBundle.getMessage(CreatePortableCasePanel.class, "CreatePortableCasePanel.jLabel2.text")); // NOI18N + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(jLabel1) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(layout.createSequentialGroup() + .addComponent(jLabel2) + .addGap(0, 0, Short.MAX_VALUE)) + .addComponent(jScrollPane1)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(deselectAllButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(selectAllButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) + .addGroup(layout.createSequentialGroup() + .addComponent(outputFolderTextField) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(chooseOutputFolderButton))) + .addContainerGap()) + ); + + layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {deselectAllButton, selectAllButton}); + + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel1) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(selectAllButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(deselectAllButton)) + .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 132, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(layout.createSequentialGroup() + .addComponent(jLabel2) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(outputFolderTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(chooseOutputFolderButton)) + .addContainerGap()) + ); + }// //GEN-END:initComponents + + private void selectAllButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_selectAllButtonActionPerformed + for (TagName tagName : tagNames) { + tagNameSelections.put(tagName.getDisplayName(), Boolean.TRUE); + } + tagNamesListBox.repaint(); + }//GEN-LAST:event_selectAllButtonActionPerformed + + private void deselectAllButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deselectAllButtonActionPerformed + for (TagName tagName : tagNames) { + tagNameSelections.put(tagName.getDisplayName(), Boolean.FALSE); + } + tagNamesListBox.repaint(); + }//GEN-LAST:event_deselectAllButtonActionPerformed + + private void chooseOutputFolderButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chooseOutputFolderButtonActionPerformed + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + + File currentSelection = new File(outputFolderTextField.getText()); + if (currentSelection.exists()) { + fileChooser.setCurrentDirectory(currentSelection); + } + + int result = fileChooser.showOpenDialog(this); + + if (result == JFileChooser.APPROVE_OPTION) { + String outputDir = fileChooser.getSelectedFile().getAbsolutePath(); + outputFolderTextField.setText(outputDir); + } + }//GEN-LAST:event_chooseOutputFolderButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton chooseOutputFolderButton; + private javax.swing.JButton deselectAllButton; + private javax.swing.JLabel jLabel1; + private javax.swing.JLabel jLabel2; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JTextField outputFolderTextField; + private javax.swing.JButton selectAllButton; + private javax.swing.JList tagNamesListBox; + // End of variables declaration//GEN-END:variables +} diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryDatabaseUpdateService.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryDatabaseUpdateService.java index 62cb31153a..f27c26c80f 100755 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryDatabaseUpdateService.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryDatabaseUpdateService.java @@ -63,7 +63,7 @@ public class ImageGalleryDatabaseUpdateService implements AutopsyService { return; } ProgressIndicator progress = context.getProgressIndicator(); - progress.start(Bundle.ImageGalleryDatabaseUpdateService_openCaseResources_progressMessage_start()); + progress.progress(Bundle.ImageGalleryDatabaseUpdateService_openCaseResources_progressMessage_start()); try { ImageGalleryModule.createController(context.getCase()); } catch (TskCoreException ex) { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java index 8553c4e3f0..427c729a8b 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java @@ -1577,11 +1577,20 @@ public final class DrawableDB { return map; } - + /** + * Get the build status for the given data source. + * Will return UNKNOWN if the data source is not yet in the database. + * + * @param dataSourceId + * + * @return The status of the data source or UKNOWN if it is not found. + * + * @throws TskCoreException + */ public DrawableDbBuildStatusEnum getDataSourceDbBuildStatus(Long dataSourceId) throws TskCoreException { Map statusMap = getDataSourceDbBuildStatus(); if (statusMap.containsKey(dataSourceId) == false) { - throw new TskCoreException("Data Source ID not found: " + dataSourceId); + return DrawableDbBuildStatusEnum.UNKNOWN; } return statusMap.get(dataSourceId); } diff --git a/docs/doxygen-user/footer.html b/docs/doxygen-user/footer.html index e53036dde6..5ab86c6e86 100644 --- a/docs/doxygen-user/footer.html +++ b/docs/doxygen-user/footer.html @@ -1,5 +1,5 @@
-

Copyright © 2012-2018 Basis Technology. Generated on $date
+

Copyright © 2012-2019 Basis Technology. Generated on $date
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.

diff --git a/docs/doxygen-user/main.dox b/docs/doxygen-user/main.dox index e22d5fb20e..231b9dc95c 100644 --- a/docs/doxygen-user/main.dox +++ b/docs/doxygen-user/main.dox @@ -65,6 +65,7 @@ The following topics are available here: - \subpage live_triage_page - \subpage advanced_page - \subpage experimental_page +- \subpage translations_page If the topic you need is not listed, refer to the Autopsy Wiki or join the SleuthKit User List at SourceForge. diff --git a/docs/doxygen-user/translations.dox b/docs/doxygen-user/translations.dox new file mode 100644 index 0000000000..6ff49fa558 --- /dev/null +++ b/docs/doxygen-user/translations.dox @@ -0,0 +1,75 @@ +/*! \page translations_page Translating This Document + +The Autopsy user base is global. You can help out by translating the UI and this documentation. + +\section translations_doc Translating Documentation + +This section outlines how to translate this user documentation. To translate, you will need: +- A git account +- Basic familiarity with git +- Text editor + +The Autopsy documentation is created by [Doxygen](http://www.doxygen.nl/) from ".dox" text files in the [docs/doxygen-user](https://github.com/sleuthkit/autopsy/tree/develop/docs/doxygen-user) folder in the Github repository. + +The first step is to fork the [Autopsy Repository](https://github.com/sleuthkit/autopsy) into your Git account and make a clone of it into your environment so that you can make edits to the files. + +As you are editing, you can review your documentation by installing Doxygen and running 'doxygen' from within the translations folder. It will save the HTML to the 'user-docs' folder. + +\subsection translations_doc_start Translating To a New Language + +If there is not already documentation in a language, then you need to make a copy of the entire English 'doxygen-user' folder and name it 'doxygen-user_AB' where AB is replaced by the 2 character [country code] (http://www.lingoes.net/en/translator/langcode.htm). For example, 'doxygen-user_fr' for French and 'doxygen-user_ja' for Japanese. + +Edit the Doxyfile to update the OUTPUT_LANGUAGE field. For English it has: + +\code +OUTPUT_LANGUAGE = English +\endcode + +Now, simply start translating the English documents. + +\subsection translations_doc_update Updating The Documentation + +When new releases are made and the English documentation is updated, the other languages should be updated as well. To determine what has changed: +- First, determine when the last time the documentation was changed. From a command line, you can change into the translated documentation folder and type: + +\code + $ cd docs/doxygen-user_fr + $ git log -n 1 . + commit 94e4b1042af47908dd4a0b2959b3f6c3d4af1111 + Author: John Doe + Date: Tue Jan 1 22:56:09 2019 -0500 + + update to quick start +\endcode + +This shows you that commit 94e4b1042af47908dd4a0b2959b3f6c3d4af1111 was the last translation update to occur for the French version. + +- Next, determine what changed in the English version since then: + +\code + $ git diff 94e4b1042af47908dd4a0b2959b3f6c3d4af1111 ../doxygen-user + diff --git a/docs/doxygen-user/central_repo.dox b/docs/doxygen-user/central_repo.dox +index 83d3407e8..e8cd01c1b 100644 + --- a/docs/doxygen-user/central_repo.dox + +++ b/docs/doxygen-user/central_repo.dox + @@ -79,6 +79,16 @@ Descriptions of the property types: + - Phone numbers are currently only extracted from call logs, contact lists and message, which come from the Android Analyzer module. + - USB Devices + - USB device properties come from the registry parsing in the Recent Activity Module. + +- Wireless Networks + + - Wireless networks are correlated on SSIDs, and come from the registry par +\endcode + +- Update the translated documentation accordingly based on what changed in the English version. + +- If you do not get to complete all of the changes, you should create a TODO.txt file that lists what was not updated so that other people know that not everything was updated. + +\subsection translations_doc_commit Committing the Documentation + +You should submit a Github Pull Request when: +- You complete a language. +- You don't have time to do more work, but want to submit what you did. + +To get the code committed, send a [pull request](https://help.github.com/articles/about-pull-requests/) to the main Autopsy repository. + +*/ \ No newline at end of file diff --git a/setupSleuthkitBranch.py b/setupSleuthkitBranch.py index 4c7ace32ac..881ce44570 100644 --- a/setupSleuthkitBranch.py +++ b/setupSleuthkitBranch.py @@ -39,17 +39,26 @@ def gitSleuthkitCheckout(branch, branchOwner): # passed is a global variable that gets set to non-zero integer # When an error occurs global passed - if (branchOwner==ORIGIN_OWNER): - cmd = ['git','checkout', branch] - else: + if branch in getSleuthkitBranchList(branchOwner): #add the remotes + #if the branch owner was origin substitute in the name of that owner + if (branchOwner==ORIGIN_OWNER): + gitHubUser="sleuthkit" + else: + gitHubUser=branchOwner checkout=['git','checkout','-b',branchOwner+'-'+branch] + print("Command run:" + " ".join(checkout)) passed = subprocess.call(checkout, stdout=sys.stdout,cwd=TSK_HOME) - cmd = ['git','pull', "/".join(["https://github.com", branchOwner, "sleuthkit.git"]), branch] + cmd = ['git','pull', "/".join(["https://github.com", gitHubUser, "sleuthkit.git"]), branch] if passed != 0: #0 would be success #unable to create new branch return instead of pulling return - passed = subprocess.call(cmd,stdout=sys.stdout,cwd=TSK_HOME) + print("Command run:" + " ".join(cmd)) + passed = subprocess.call(cmd,stdout=sys.stdout,cwd=TSK_HOME) + if (passed == 0): + sys.exit() #exit if successful + else: + print("Branch: " + branch + " does not exist for owner: " + branchOwner) def parseXML(xmlFile): ''' @@ -70,40 +79,39 @@ def main(): if not TSK_HOME: sys.exit(1) print('Please set TSK_HOME env variable') - # Get the Autopsy branch being used. Travis and Appveyor # will tell us where a pull request is directed TRAVIS=os.getenv("TRAVIS",False) APPVEYOR=os.getenv("APPVEYOR",False) if TRAVIS == "true": - CURRENT_BRANCH=os.getenv("TRAVIS_PULL_REQUEST_BRANCH",False) - BRANCH_OWNER=os.getenv("TRAVIS_PULL_REQUEST_SLUG", False).split('/')[0] + CURRENT_BRANCH=os.getenv("TRAVIS_PULL_REQUEST_BRANCH","") #make default empty string which is same vaule used when not a PR + if (CURRENT_BRANCH != ""): #if it is a PR + BRANCH_OWNER=os.getenv("TRAVIS_PULL_REQUEST_SLUG", ORIGIN_OWNER+"/"+CURRENT_BRANCH).split('/')[0] #default owner is ORIGIN_OWNER + gitSleuthkitCheckout(CURRENT_BRANCH, BRANCH_OWNER) + TARGET_BRANCH=os.getenv("TRAVIS_BRANCH",DEVELOP_BRANCH) elif APPVEYOR: - CURRENT_BRANCH=os.getenv("APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH",False) - BRANCH_OWNER=os.getenv("APPVEYOR_PULL_REQUEST_HEAD_REPO_NAME", False).split('/')[0] + CURRENT_BRANCH=os.getenv("APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH","") #make default same as value used by travis for readability of code + if (CURRENT_BRANCH != ""): #if it is a PR + BRANCH_OWNER=os.getenv("APPVEYOR_PULL_REQUEST_HEAD_REPO_NAME", ORIGIN_OWNER+"/"+CURRENT_BRANCH).split('/')[0] #default owner is ORIGIN_OWNER + gitSleuthkitCheckout(CURRENT_BRANCH, BRANCH_OWNER) + TARGET_BRANCH=os.getenv("APPVEYOR_REPO_BRANCH",DEVELOP_BRANCH) else: cmd=['git','rev-parse','--abbrev-ref','HEAD'] output = subprocess.check_output(cmd) - CURRENT_BRANCH=output.strip() - BRANCH_OWNER=ORIGIN_OWNER + TARGET_BRANCH=output.strip() # If we are in an Autopsy release branch, then use the # info in TSKVersion.xml to find the corresponding TSK # release branch. For other branches, we don't always # trust that TSKVersion has been updated. - if CURRENT_BRANCH.startswith('release'): + if TARGET_BRANCH.startswith('release'): version = parseXML('TSKVersion.xml') RELEASE_BRANCH = "release-"+version - gitSleuthkitCheckout(RELEASE_BRANCH, BRANCH_OWNER) - #If it failed try the origin release branch - if passed != 0: - gitSleuthkitCheckout(RELEASE_BRANCH, ORIGIN_OWNER) - # Check if the same branch exists in TSK (develop->develop, custom1->custom1, etc.) + #Check if the same user has a release branch which corresponds to this release branch + gitSleuthkitCheckout(RELEASE_BRANCH, ORIGIN_OWNER) else: - gitSleuthkitCheckout(CURRENT_BRANCH, BRANCH_OWNER) - + gitSleuthkitCheckout(TARGET_BRANCH, ORIGIN_OWNER) # Otherwise, default to origin develop - if passed != 0: - gitSleuthkitCheckout(DEVELOP_BRANCH, ORIGIN_OWNER) + gitSleuthkitCheckout(DEVELOP_BRANCH, ORIGIN_OWNER) if passed != 0: print('Error checking out a Sleuth Kit branch')