diff --git a/.gitignore b/.gitignore index 0f00c6c00f..6f6f3fb653 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ genfiles.properties !/test/input/NSRL.txt-md5.idx /test/output/* !/test/output/gold +/test/script/output_dir_link.txt /test/output/gold/tmp /test/script/ScriptLog.txt /test/script/__pycache__/ diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindowProvider.java b/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindowProvider.java index 1b30f50e60..a922a6f19d 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindowProvider.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindowProvider.java @@ -21,7 +21,11 @@ package org.sleuthkit.autopsy.casemodule; import java.util.Collection; import java.util.Iterator; import java.util.logging.Level; +import org.netbeans.spi.sendopts.OptionProcessor; import org.openide.util.Lookup; +import org.sleuthkit.autopsy.commandlineingest.CommandLineIngestManager; +import org.sleuthkit.autopsy.commandlineingest.CommandLineOptionProcessor; +import org.sleuthkit.autopsy.commandlineingest.CommandLineStartupWindow; import org.sleuthkit.autopsy.coreutils.Logger; /** @@ -54,6 +58,17 @@ public class StartupWindowProvider implements StartupWindowInterface { private void init() { if (startupWindowToUse == null) { + // first check whether we are running from command line + if (isRunningFromCommandLine()) { + // Autopsy is running from command line + logger.log(Level.INFO, "Running from command line"); //NON-NLS + System.out.println("Running from command line"); + startupWindowToUse = new CommandLineStartupWindow(); + // kick off command line processing + new CommandLineIngestManager().start(); + return; + } + //discover the registered windows Collection startupWindows = Lookup.getDefault().lookupAll(StartupWindowInterface.class); @@ -93,6 +108,30 @@ public class StartupWindowProvider implements StartupWindowInterface { } } + /** + * Checks whether Autopsy is running from command line. There is an + * OptionProcessor that is responsible for processing command line inputs. + * If Autopsy is indeed running from command line, then use the command line + * startup window. + * + * @return True if running from command line, false otherwise + */ + private boolean isRunningFromCommandLine() { + + // first look up all OptionProcessors and see if running from command line option is set + Collection optionProcessors = Lookup.getDefault().lookupAll(OptionProcessor.class); + Iterator optionsIterator = optionProcessors.iterator(); + while (optionsIterator.hasNext()) { + // find CommandLineOptionProcessor + OptionProcessor processor = optionsIterator.next(); + if ((processor instanceof CommandLineOptionProcessor)) { + // check if we are running from command line + return ((CommandLineOptionProcessor) processor).isRunFromCommandLine(); + } + } + return false; + } + @Override public void open() { if (startupWindowToUse != null) { diff --git a/Core/src/org/sleuthkit/autopsy/commandlineingest/AddDataSourceCallback.java b/Core/src/org/sleuthkit/autopsy/commandlineingest/AddDataSourceCallback.java new file mode 100755 index 0000000000..b4a000b0f3 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/commandlineingest/AddDataSourceCallback.java @@ -0,0 +1,96 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.commandlineingest; + +import java.util.List; +import java.util.UUID; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; +import org.sleuthkit.datamodel.Content; + + +/** + * A "callback" that collects the results of running a data source processor on + * a data source and unblocks the job processing thread when the data source + * processor finishes running in its own thread. + */ +class AddDataSourceCallback extends DataSourceProcessorCallback { + + private final Case caseForJob; + private final DataSource dataSourceInfo; + private final UUID taskId; + private final Object lock; + + /** + * Constructs a "callback" that collects the results of running a data + * source processor on a data source and unblocks the job processing thread + * when the data source processor finishes running in its own thread. + * + * @param caseForJob The case for the current job. + * @param dataSourceInfo The data source + * @param taskId The task id to associate with ingest job events. + */ + AddDataSourceCallback(Case caseForJob, DataSource dataSourceInfo, UUID taskId, Object lock) { + this.caseForJob = caseForJob; + this.dataSourceInfo = dataSourceInfo; + this.taskId = taskId; + this.lock = lock; + } + + /** + * Called by the data source processor when it finishes running in its own + * thread. + * + * @param result The result code for the processing of the data source. + * @param errorMessages Any error messages generated during the processing + * of the data source. + * @param dataSourceContent The content produced by processing the data + * source. + */ + @Override + public void done(DataSourceProcessorCallback.DataSourceProcessorResult result, List errorMessages, List dataSourceContent) { + if (!dataSourceContent.isEmpty()) { + caseForJob.notifyDataSourceAdded(dataSourceContent.get(0), taskId); + } else { + caseForJob.notifyFailedAddingDataSource(taskId); + } + dataSourceInfo.setDataSourceProcessorOutput(result, errorMessages, dataSourceContent); + dataSourceContent.addAll(dataSourceContent); + synchronized (lock) { + lock.notifyAll(); + } + } + + /** + * Called by the data source processor when it finishes running in its own + * thread, if that thread is the AWT (Abstract Window Toolkit) event + * dispatch thread (EDT). + * + * @param result The result code for the processing of the data source. + * @param errorMessages Any error messages generated during the processing + * of the data source. + * @param dataSourceContent The content produced by processing the data + * source. + */ + @Override + public void doneEDT(DataSourceProcessorCallback.DataSourceProcessorResult result, List errorMessages, List dataSources) { + done(result, errorMessages, dataSources); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/commandlineingest/Bundle.properties b/Core/src/org/sleuthkit/autopsy/commandlineingest/Bundle.properties new file mode 100755 index 0000000000..8675363370 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/commandlineingest/Bundle.properties @@ -0,0 +1,17 @@ +OpenIDE-Module-Name=CommandLineAutopsy +OptionsCategory_Keywords_Command_Line_Ingest_Settings=Command Line Ingest Settings +OptionsCategory_Keywords_General=Options +OptionsCategory_Name_Command_Line_Ingest=Command Line Ingest +CommandLineIngestSettingsPanel.ResultsDirectoryUnspecified=Output folder must be set +CommandLineIngestSettingsPanel.PathInvalid=Path is not valid +CommandLineIngestSettingsPanel.CannotAccess=Cannot access +CommandLineIngestSettingsPanel.CheckPermissions=Check permissions. +CommandLineIngestSettingsPanel.jLabelSelectOutputFolder.text=Select output folder: +CommandLineIngestSettingsPanel.jLabelInvalidResultsFolder.text=jLabelInvalidOutputFolder +CommandLineIngestSettingsPanel.outputPathTextField.toolTipText=Output folder for command line processing, i.e., the location where case folder will be created by command line processing mode. +CommandLineIngestSettingsPanel.outputPathTextField.text= +CommandLineIngestSettingsPanel.browseOutputFolderButton.text=Browse +CommandLineIngestSettingsPanel.bnEditIngestSettings.toolTipText=Ingest job settings for the command line processing mode context. +CommandLineIngestSettingsPanel.bnEditIngestSettings.text=Ingest Module Settings +CommandLinePanel.jLabel1.text=Ingest is running from command line +CommandLineStartupWindow.title.text=Running in Command Line Mode diff --git a/Core/src/org/sleuthkit/autopsy/commandlineingest/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/commandlineingest/Bundle.properties-MERGED new file mode 100755 index 0000000000..8675363370 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/commandlineingest/Bundle.properties-MERGED @@ -0,0 +1,17 @@ +OpenIDE-Module-Name=CommandLineAutopsy +OptionsCategory_Keywords_Command_Line_Ingest_Settings=Command Line Ingest Settings +OptionsCategory_Keywords_General=Options +OptionsCategory_Name_Command_Line_Ingest=Command Line Ingest +CommandLineIngestSettingsPanel.ResultsDirectoryUnspecified=Output folder must be set +CommandLineIngestSettingsPanel.PathInvalid=Path is not valid +CommandLineIngestSettingsPanel.CannotAccess=Cannot access +CommandLineIngestSettingsPanel.CheckPermissions=Check permissions. +CommandLineIngestSettingsPanel.jLabelSelectOutputFolder.text=Select output folder: +CommandLineIngestSettingsPanel.jLabelInvalidResultsFolder.text=jLabelInvalidOutputFolder +CommandLineIngestSettingsPanel.outputPathTextField.toolTipText=Output folder for command line processing, i.e., the location where case folder will be created by command line processing mode. +CommandLineIngestSettingsPanel.outputPathTextField.text= +CommandLineIngestSettingsPanel.browseOutputFolderButton.text=Browse +CommandLineIngestSettingsPanel.bnEditIngestSettings.toolTipText=Ingest job settings for the command line processing mode context. +CommandLineIngestSettingsPanel.bnEditIngestSettings.text=Ingest Module Settings +CommandLinePanel.jLabel1.text=Ingest is running from command line +CommandLineStartupWindow.title.text=Running in Command Line Mode diff --git a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestManager.java b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestManager.java new file mode 100755 index 0000000000..13b76a5eca --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestManager.java @@ -0,0 +1,613 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.commandlineingest; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.File; +import java.io.FilenameFilter; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.UUID; +import java.util.Collection; +import java.util.Iterator; +import java.util.logging.Level; +import org.netbeans.spi.sendopts.OptionProcessor; +import org.openide.LifecycleManager; +import org.openide.util.Lookup; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.CaseActionException; +import org.sleuthkit.autopsy.casemodule.CaseDetails; +import org.sleuthkit.autopsy.casemodule.CaseMetadata; +import org.sleuthkit.autopsy.core.RuntimeProperties; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; +import static org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.TimeStampUtils; +import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor; +import org.sleuthkit.autopsy.events.AutopsyEvent; +import org.sleuthkit.autopsy.ingest.IngestJob; +import org.sleuthkit.autopsy.ingest.IngestJobSettings; +import org.sleuthkit.autopsy.ingest.IngestJobStartResult; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.IngestModuleError; +import org.sleuthkit.autopsy.report.ReportProgressPanel; +import org.sleuthkit.autopsy.report.caseuco.CaseUcoFormatExporter; +import org.sleuthkit.autopsy.report.caseuco.ReportCaseUco; +import org.sleuthkit.datamodel.Content; + +/** + * Allows Autopsy to be invoked with a command line arguments. Causes Autopsy to + * create a case, add a specified data source, run ingest on that data source, + * produce a CASE/UCO report and exit. + */ +public class CommandLineIngestManager { + + private static final Logger LOGGER = Logger.getLogger(CommandLineIngestManager.class.getName()); + private Path rootOutputDirectory; + + public CommandLineIngestManager() { + } + + public void start() { + new Thread(new JobProcessingTask()).start(); + } + + public void stop() { + try { + // close current case if there is one open + Case.closeCurrentCase(); + } catch (CaseActionException ex) { + LOGGER.log(Level.WARNING, "Unable to close the case while shutting down command line ingest manager", ex); //NON-NLS + } + + // shut down Autopsy + LifecycleManager.getDefault().exit(); + } + + private final class JobProcessingTask implements Runnable { + + private final Object ingestLock; + + private JobProcessingTask() { + ingestLock = new Object(); + try { + RuntimeProperties.setRunningWithGUI(false); + LOGGER.log(Level.INFO, "Set running with desktop GUI runtime property to false"); + } catch (RuntimeProperties.RuntimePropertiesException ex) { + LOGGER.log(Level.SEVERE, "Failed to set running with desktop GUI runtime property to false", ex); + } + } + + public void run() { + LOGGER.log(Level.INFO, "Job processing task started"); + + try { + // read command line inputs + LOGGER.log(Level.INFO, "Autopsy is running from command line"); //NON-NLS + String dataSourcePath = ""; + String baseCaseName = ""; + + // first look up all OptionProcessors and get input data from CommandLineOptionProcessor + Collection optionProcessors = Lookup.getDefault().lookupAll(OptionProcessor.class); + Iterator optionsIterator = optionProcessors.iterator(); + while (optionsIterator.hasNext()) { + // find CommandLineOptionProcessor + OptionProcessor processor = optionsIterator.next(); + if (processor instanceof CommandLineOptionProcessor) { + // check if we are running from command line + dataSourcePath = ((CommandLineOptionProcessor) processor).getPathToDataSource(); + baseCaseName = ((CommandLineOptionProcessor) processor).getBaseCaseName(); + } + } + + LOGGER.log(Level.INFO, "Data source path = {0}", dataSourcePath); //NON-NLS + LOGGER.log(Level.INFO, "Case name = {0}", baseCaseName); //NON-NLS + System.out.println("Data source path = " + dataSourcePath); + System.out.println("Case name = " + baseCaseName); + + // verify inputs + if (dataSourcePath.isEmpty()) { + LOGGER.log(Level.SEVERE, "Data source path not specified"); + System.out.println("Data source path not specified"); + return; + } + + if (baseCaseName.isEmpty()) { + LOGGER.log(Level.SEVERE, "Case name not specified"); + System.out.println("Case name not specified"); + return; + } + + if (!(new File(dataSourcePath).exists())) { + LOGGER.log(Level.SEVERE, "Data source file not found {0}", dataSourcePath); + System.out.println("Data source file not found " + dataSourcePath); + return; + } + + // read options panel configuration + String rootOutputDir = UserPreferences.getCommandLineModeResultsFolder(); + LOGGER.log(Level.INFO, "Output directory = {0}", rootOutputDir); //NON-NLS + System.out.println("Output directoryh = " + rootOutputDir); + + if (rootOutputDir.isEmpty()) { + LOGGER.log(Level.SEVERE, "Output directory not specified, please configure Command Line Options Panel (in Tools -> Options)"); + System.out.println("Output directory not specified, please configure Command Line Options Panel (in Tools -> Options)"); + return; + } + + if (!(new File(rootOutputDir).exists())) { + LOGGER.log(Level.SEVERE, "The output directory doesn't exist {0}", rootOutputDir); + System.out.println("The output directory doesn't exist " + rootOutputDir); + return; + } + rootOutputDirectory = Paths.get(rootOutputDir); + + // open case + Case caseForJob; + try { + caseForJob = openCase(baseCaseName); + } catch (CaseActionException ex) { + LOGGER.log(Level.SEVERE, "Error creating or opening case " + baseCaseName, ex); + System.out.println("Error creating or opening case " + baseCaseName); + return; + } + + if (caseForJob == null) { + LOGGER.log(Level.SEVERE, "Error creating or opening case {0}", baseCaseName); + System.out.println("Error creating or opening case " + baseCaseName); + return; + } + + DataSource dataSource = new DataSource("", Paths.get(dataSourcePath)); + try { + // run data source processor + runDataSourceProcessor(caseForJob, dataSource); + + // run ingest manager + analyze(dataSource); + + // generate CASE-UCO report + Long selectedDataSourceId = getDataSourceId(dataSource); + Path reportFolderPath = Paths.get(caseForJob.getReportDirectory(), "CASE-UCO", "Data_Source_ID_" + selectedDataSourceId.toString() + "_" + TimeStampUtils.createTimeStamp(), ReportCaseUco.getReportFileName()); //NON_NLS + ReportProgressPanel progressPanel = new ReportProgressPanel("CASE_UCO", rootOutputDir); // dummy progress panel + CaseUcoFormatExporter.generateReport(selectedDataSourceId, reportFolderPath.toString(), progressPanel); + } catch (InterruptedException | AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException | AnalysisStartupException ex) { + LOGGER.log(Level.SEVERE, "Unable to ingest data source " + dataSourcePath + ". Exiting...", ex); + System.out.println("Unable to ingest data source " + dataSourcePath + ". Exiting..."); + } catch (Throwable ex) { + /* + * Unexpected runtime exceptions firewall. This task is designed to + * be able to be run in an executor service thread pool without + * calling get() on the task's Future, so this ensures that + * such errors get logged. + */ + LOGGER.log(Level.SEVERE, "Unexpected error while ingesting data source " + dataSourcePath, ex); + System.out.println("Unexpected error while ingesting data source " + dataSourcePath + ". Exiting..."); + + } finally { + try { + Case.closeCurrentCase(); + } catch (CaseActionException ex) { + LOGGER.log(Level.WARNING, "Exception while closing case", ex); + System.out.println("Exception while closing case"); + } + } + + } finally { + LOGGER.log(Level.INFO, "Job processing task finished"); + System.out.println("Job processing task finished"); + + // shut down Autopsy + stop(); + } + } + + /** + * Provides object ID of the data source by reading it from Content + * object. + * + * @param dataSource DataSource object + * @return object ID + */ + private Long getDataSourceId(DataSource dataSource) { + Content content = dataSource.getContent().get(0); + return content.getId(); + } + + private Case openCase(String baseCaseName) throws CaseActionException { + + LOGGER.log(Level.INFO, "Opening case {0}", baseCaseName); + + Path caseDirectoryPath = findCaseDirectory(rootOutputDirectory, baseCaseName); + if (null != caseDirectoryPath) { + // found an existing case directory for same case name. the input case name must be unique. Exit. + LOGGER.log(Level.SEVERE, "Case {0} already exists. Case name must be unique. Exiting", baseCaseName); + throw new CaseActionException("Case " + baseCaseName + " already exists. Case name must be unique. Exiting"); + } else { + caseDirectoryPath = createCaseFolderPath(rootOutputDirectory, baseCaseName); + + // Create the case directory + Case.createCaseDirectory(caseDirectoryPath.toString(), Case.CaseType.SINGLE_USER_CASE); + + CaseDetails caseDetails = new CaseDetails(baseCaseName); + Case.createAsCurrentCase(Case.CaseType.SINGLE_USER_CASE, caseDirectoryPath.toString(), caseDetails); + } + + Case caseForJob = Case.getCurrentCase(); + LOGGER.log(Level.INFO, "Opened case {0}", caseForJob.getName()); + return caseForJob; + } + + /** + * Passes the data source for the current job through a data source + * processor that adds it to the case database. + * + * @param dataSource The data source. + * + * @throws + * AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException + * if there was a DSP processing error + * + * @throws InterruptedException if the thread running the job processing + * task is interrupted while blocked, i.e., if auto ingest is shutting + * down. + */ + private void runDataSourceProcessor(Case caseForJob, DataSource dataSource) throws InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException { + + LOGGER.log(Level.INFO, "Adding data source {0} ", dataSource.getPath().toString()); + + // Get an ordered list of data source processors to try + List validDataSourceProcessors; + try { + validDataSourceProcessors = DataSourceProcessorUtility.getOrderedListOfDataSourceProcessors(dataSource.getPath()); + } catch (AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException ex) { + LOGGER.log(Level.SEVERE, "Exception while determining best data source processor for {0}", dataSource.getPath()); + // rethrow the exception. + throw ex; + } + + // did we find a data source processor that can process the data source + if (validDataSourceProcessors.isEmpty()) { + // This should never happen. We should add all unsupported data sources as logical files. + LOGGER.log(Level.SEVERE, "Unsupported data source {0}", dataSource.getPath()); // NON-NLS + return; + } + + DataSourceProcessorProgressMonitor progressMonitor = new DoNothingDSPProgressMonitor(); + synchronized (ingestLock) { + // Try each DSP in decreasing order of confidence + for (AutoIngestDataSourceProcessor selectedProcessor : validDataSourceProcessors) { + UUID taskId = UUID.randomUUID(); + caseForJob.notifyAddingDataSource(taskId); + DataSourceProcessorCallback callBack = new AddDataSourceCallback(caseForJob, dataSource, taskId, ingestLock); + caseForJob.notifyAddingDataSource(taskId); + LOGGER.log(Level.INFO, "Identified data source type for {0} as {1}", new Object[]{dataSource.getPath(), selectedProcessor.getDataSourceType()}); + selectedProcessor.process(dataSource.getDeviceId(), dataSource.getPath(), progressMonitor, callBack); + ingestLock.wait(); + + // at this point we got the content object(s) from the current DSP. + // check whether the data source was processed successfully + if ((dataSource.getResultDataSourceProcessorResultCode() == CRITICAL_ERRORS) + || dataSource.getContent().isEmpty()) { + // move onto the the next DSP that can process this data source + logDataSourceProcessorResult(dataSource); + continue; + } + + logDataSourceProcessorResult(dataSource); + return; + } + // If we get to this point, none of the processors were successful + LOGGER.log(Level.SEVERE, "All data source processors failed to process {0}", dataSource.getPath()); + // Throw an exception. It will get caught & handled upstream and will result in AIM auto-pause. + throw new AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException("Failed to process " + dataSource.getPath() + " with all data source processors"); + } + } + + /** + * Logs the results of running a data source processor on the data + * source for the current job. + * + * @param dataSource The data source. + */ + private void logDataSourceProcessorResult(DataSource dataSource) { + + DataSourceProcessorCallback.DataSourceProcessorResult resultCode = dataSource.getResultDataSourceProcessorResultCode(); + if (null != resultCode) { + switch (resultCode) { + case NO_ERRORS: + LOGGER.log(Level.INFO, "Added data source to case"); + if (dataSource.getContent().isEmpty()) { + LOGGER.log(Level.SEVERE, "Data source failed to produce content"); + } + break; + + case NONCRITICAL_ERRORS: + for (String errorMessage : dataSource.getDataSourceProcessorErrorMessages()) { + LOGGER.log(Level.WARNING, "Non-critical error running data source processor for {0}: {1}", new Object[]{dataSource.getPath(), errorMessage}); + } + LOGGER.log(Level.INFO, "Added data source to case"); + if (dataSource.getContent().isEmpty()) { + LOGGER.log(Level.SEVERE, "Data source failed to produce content"); + } + break; + + case CRITICAL_ERRORS: + for (String errorMessage : dataSource.getDataSourceProcessorErrorMessages()) { + LOGGER.log(Level.SEVERE, "Critical error running data source processor for {0}: {1}", new Object[]{dataSource.getPath(), errorMessage}); + } + LOGGER.log(Level.SEVERE, "Failed to add data source to case"); + break; + } + } else { + LOGGER.log(Level.WARNING, "No result code for data source processor for {0}", dataSource.getPath()); + } + } + + /** + * Analyzes the data source content returned by the data source + * processor using the configured set of data source level and file + * level analysis modules. + * + * @param dataSource The data source to analyze. + * + * @throws AnalysisStartupException if there is an error analyzing the + * data source. + * @throws InterruptedException if the thread running the job processing + * task is interrupted while blocked, i.e., if auto ingest is shutting + * down. + */ + private void analyze(DataSource dataSource) throws AnalysisStartupException, InterruptedException { + + LOGGER.log(Level.INFO, "Starting ingest modules analysis for {0} ", dataSource.getPath()); + IngestJobEventListener ingestJobEventListener = new IngestJobEventListener(); + IngestManager.getInstance().addIngestJobEventListener(ingestJobEventListener); + try { + synchronized (ingestLock) { + IngestJobSettings ingestJobSettings = new IngestJobSettings(UserPreferences.getCommandLineModeIngestModuleContextString()); + List settingsWarnings = ingestJobSettings.getWarnings(); + if (settingsWarnings.isEmpty()) { + IngestJobStartResult ingestJobStartResult = IngestManager.getInstance().beginIngestJob(dataSource.getContent(), ingestJobSettings); + IngestJob ingestJob = ingestJobStartResult.getJob(); + if (null != ingestJob) { + /* + * Block until notified by the ingest job event + * listener or until interrupted because auto ingest + * is shutting down. + */ + ingestLock.wait(); + LOGGER.log(Level.INFO, "Finished ingest modules analysis for {0} ", dataSource.getPath()); + IngestJob.ProgressSnapshot jobSnapshot = ingestJob.getSnapshot(); + for (IngestJob.ProgressSnapshot.DataSourceProcessingSnapshot snapshot : jobSnapshot.getDataSourceSnapshots()) { + if (!snapshot.isCancelled()) { + List cancelledModules = snapshot.getCancelledDataSourceIngestModules(); + if (!cancelledModules.isEmpty()) { + LOGGER.log(Level.WARNING, String.format("Ingest module(s) cancelled for %s", dataSource.getPath())); + for (String module : snapshot.getCancelledDataSourceIngestModules()) { + LOGGER.log(Level.WARNING, String.format("%s ingest module cancelled for %s", module, dataSource.getPath())); + } + } + LOGGER.log(Level.INFO, "Analysis of data source completed"); + } else { + LOGGER.log(Level.WARNING, "Analysis of data source cancelled"); + IngestJob.CancellationReason cancellationReason = snapshot.getCancellationReason(); + if (IngestJob.CancellationReason.NOT_CANCELLED != cancellationReason && IngestJob.CancellationReason.USER_CANCELLED != cancellationReason) { + throw new AnalysisStartupException(String.format("Analysis cancelled due to %s for %s", cancellationReason.getDisplayName(), dataSource.getPath())); + } + } + } + } else if (!ingestJobStartResult.getModuleErrors().isEmpty()) { + for (IngestModuleError error : ingestJobStartResult.getModuleErrors()) { + LOGGER.log(Level.SEVERE, String.format("%s ingest module startup error for %s", error.getModuleDisplayName(), dataSource.getPath()), error.getThrowable()); + } + LOGGER.log(Level.SEVERE, "Failed to analyze data source due to ingest job startup error"); + throw new AnalysisStartupException(String.format("Error(s) during ingest module startup for %s", dataSource.getPath())); + } else { + LOGGER.log(Level.SEVERE, String.format("Ingest manager ingest job start error for %s", dataSource.getPath()), ingestJobStartResult.getStartupException()); + throw new AnalysisStartupException("Ingest manager error starting job", ingestJobStartResult.getStartupException()); + } + } else { + for (String warning : settingsWarnings) { + LOGGER.log(Level.SEVERE, "Ingest job settings error for {0}: {1}", new Object[]{dataSource.getPath(), warning}); + } + LOGGER.log(Level.SEVERE, "Failed to analyze data source due to settings errors"); + throw new AnalysisStartupException("Error(s) in ingest job settings"); + } + } + } finally { + IngestManager.getInstance().removeIngestJobEventListener(ingestJobEventListener); + } + } + + /** + * Creates a case folder path. Does not create the folder described by + * the path. + * + * @param caseFoldersPath The root case folders path. + * @param caseName The name of the case. + * + * @return A case folder path with a time stamp suffix. + */ + Path createCaseFolderPath(Path caseFoldersPath, String caseName) { + String folderName = caseName + "_" + TimeStampUtils.createTimeStamp(); + return Paths.get(caseFoldersPath.toString(), folderName); + } + + /** + * Searches a given folder for the most recently modified case folder + * for a case. + * + * @param folderToSearch The folder to be searched. + * @param caseName The name of the case for which a case folder is to be + * found. + * + * @return The path of the case folder, or null if it is not found. + */ + Path findCaseDirectory(Path folderToSearch, String caseName) { + File searchFolder = new File(folderToSearch.toString()); + if (!searchFolder.isDirectory()) { + return null; + } + Path caseFolderPath = null; + String[] candidateFolders = searchFolder.list(new CaseFolderFilter(caseName)); + long mostRecentModified = 0; + for (String candidateFolder : candidateFolders) { + File file = new File(candidateFolder); + if (file.lastModified() >= mostRecentModified) { + mostRecentModified = file.lastModified(); + caseFolderPath = Paths.get(folderToSearch.toString(), file.getPath()); + } + } + return caseFolderPath; + } + + /** + * An ingest job event listener that allows the job processing task to + * block until the analysis of a data source by the data source level + * and file level ingest modules is completed. + *

+ * Note that the ingest job can spawn "child" ingest jobs (e.g., if an + * embedded virtual machine is found), so the job processing task must + * remain blocked until ingest is no longer running. + */ + private class IngestJobEventListener implements PropertyChangeListener { + + /** + * Listens for local ingest job completed or cancelled events and + * notifies the job processing thread when such an event occurs and + * there are no "child" ingest jobs running. + * + * @param event + */ + @Override + public void propertyChange(PropertyChangeEvent event) { + if (AutopsyEvent.SourceType.LOCAL == ((AutopsyEvent) event).getSourceType()) { + String eventType = event.getPropertyName(); + if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { + synchronized (ingestLock) { + ingestLock.notify(); + } + } + } + } + }; + + /** + * A data source processor progress monitor does nothing. There is + * currently no mechanism for showing or recording data source processor + * progress during an ingest job. + */ + private class DoNothingDSPProgressMonitor implements DataSourceProcessorProgressMonitor { + + /** + * Does nothing. + * + * @param indeterminate + */ + @Override + public void setIndeterminate(final boolean indeterminate) { + } + + /** + * Does nothing. + * + * @param progress + */ + @Override + public void setProgress(final int progress) { + } + + /** + * Does nothing. + * + * @param text + */ + @Override + public void setProgressText(final String text) { + } + } + + /** + * Exception type thrown when there is a problem analyzing a data source + * with data source level and file level ingest modules for an ingest + * job. + */ + private final class AnalysisStartupException extends Exception { + + private static final long serialVersionUID = 1L; + + private AnalysisStartupException(String message) { + super(message); + } + + private AnalysisStartupException(String message, Throwable cause) { + super(message, cause); + } + } + } + + private static class CaseFolderFilter implements FilenameFilter { + + private final String caseName; + private final static String CASE_METADATA_EXT = CaseMetadata.getFileExtension(); + + CaseFolderFilter(String caseName) { + this.caseName = caseName; + } + + @Override + public boolean accept(File folder, String fileName) { + File file = new File(folder, fileName); + if (fileName.length() > TimeStampUtils.getTimeStampLength() && file.isDirectory()) { + if (TimeStampUtils.endsWithTimeStamp(fileName)) { + if (null != caseName) { + String fileNamePrefix = fileName.substring(0, fileName.length() - TimeStampUtils.getTimeStampLength()); + if (fileNamePrefix.equals(caseName)) { + return hasCaseMetadataFile(file); + } + } else { + return hasCaseMetadataFile(file); + } + } + } + return false; + } + + /** + * Determines whether or not there is a case metadata file in a given + * folder. + * + * @param folder The file object representing the folder to search. + * + * @return True or false. + */ + private static boolean hasCaseMetadataFile(File folder) { + for (File file : folder.listFiles()) { + if (file.getName().toLowerCase().endsWith(CASE_METADATA_EXT) && file.isFile()) { + return true; + } + } + return false; + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestSettingsPanel.form b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestSettingsPanel.form new file mode 100755 index 0000000000..2333564f89 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestSettingsPanel.form @@ -0,0 +1,162 @@ + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestSettingsPanel.java new file mode 100755 index 0000000000..20811d0fb8 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestSettingsPanel.java @@ -0,0 +1,395 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.commandlineingest; + +import java.awt.BorderLayout; +import java.awt.Cursor; +import java.io.File; +import java.nio.file.Files; +import java.util.List; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.ingest.IngestJobSettings; +import org.sleuthkit.autopsy.ingest.IngestJobSettingsPanel; +import java.nio.file.Paths; +import org.sleuthkit.autopsy.coreutils.FileUtil; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * Configuration panel for auto ingest settings. + */ +@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives +public class CommandLineIngestSettingsPanel extends javax.swing.JPanel { + + private final CommandLineIngestSettingsPanelController controller; + private final JFileChooser fc = new JFileChooser(); + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(CommandLineIngestSettingsPanel.class.getName()); + + /** + * Creates new form AutoIngestSettingsPanel + * + * @param theController Controller to notify of changes. + */ + public CommandLineIngestSettingsPanel(CommandLineIngestSettingsPanelController theController) { + controller = theController; + initComponents(); + + load(true); + outputPathTextField.getDocument().addDocumentListener(new MyDocumentListener()); + jLabelInvalidResultsFolder.setText(""); + } + + private class MyDocumentListener implements DocumentListener { + + @Override + public void changedUpdate(DocumentEvent e) { + valid(); + controller.changed(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + valid(); + controller.changed(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + valid(); + controller.changed(); + } + }; + + /** + * Load mode from persistent storage. + * + * @param inStartup True if we're doing the initial population of the UI + */ + final void load(boolean inStartup) { + + String results = org.sleuthkit.autopsy.commandlineingest.UserPreferences.getCommandLineModeResultsFolder(); + if (results != null) { + outputPathTextField.setText(results); + } else { + outputPathTextField.setText(""); + } + + valid(); + } + + /** + * Save mode to persistent storage. + */ + void store() { + String resultsFolderPath = getNormalizedFolderPath(outputPathTextField.getText().trim()); + org.sleuthkit.autopsy.commandlineingest.UserPreferences.setCommandLineModeResultsFolder(resultsFolderPath); + } + + /** + * Validate current panel settings. + */ + boolean valid() { + + if (validateResultsPath()) { + return true; + } + return false; + } + + /** + * Normalizes a path to make sure there are no "space" characters at the end + * + * @param path Path to a directory + * + * @return Path without "space" characters at the end + */ + String normalizePath(String path) { + + while (path.length() > 0) { + if (path.charAt(path.length() - 1) == ' ') { + path = path.substring(0, path.length() - 1); + } else { + break; + } + } + return path; + } + + /** + * Validates that a path is valid and points to a folder. + * + * @param path A path to be validated + * + * @return boolean returns true if valid and points to a folder, false + * otherwise + */ + boolean isFolderPathValid(String path) { + try { + File file = new File(normalizePath(path)); + + // check if it's a symbolic link + if (Files.isSymbolicLink(file.toPath())) { + return true; + } + + // local folder + if (file.exists() && file.isDirectory()) { + return true; + } + } catch (Exception ex) { + // Files.isSymbolicLink (and other "files" methods) throw exceptions on seemingly innocent inputs. + // For example, it will throw an exception when either " " is last character in path or + // a path starting with ":". + // We can just ignore these exceptions as they occur in process of user typing in the path. + return false; + } + return false; + } + + /** + * Returns a path that was normalized by file system. + * + * @param path A path to be normalized. Normalization occurs inside a call + * to new File(). + * + * @return String returns normalized OS path + */ + String getNormalizedFolderPath(String path) { + // removes "/", "\", and " " characters at the end of path string. + // normalizePath() removes spaces at the end of path and a call to "new File()" + // internally formats the path string to remove "/" and "\" characters at the end of path. + File file = new File(normalizePath(path)); + return file.getPath(); + } + + /** + * Validate results path. Display warnings if invalid. + */ + boolean validateResultsPath() { + + String outputPath = outputPathTextField.getText().trim(); + + if (outputPath.isEmpty()) { + jLabelInvalidResultsFolder.setVisible(true); + jLabelInvalidResultsFolder.setText(NbBundle.getMessage(CommandLineIngestSettingsPanel.class, "CommandLineIngestSettingsPanel.ResultsDirectoryUnspecified")); + return false; + } + + if (!isFolderPathValid(outputPath)) { + jLabelInvalidResultsFolder.setVisible(true); + jLabelInvalidResultsFolder.setText(NbBundle.getMessage(CommandLineIngestSettingsPanel.class, "CommandLineIngestSettingsPanel.PathInvalid")); + return false; + } + + if (false == permissionsAppropriate(outputPath)) { + jLabelInvalidResultsFolder.setVisible(true); + jLabelInvalidResultsFolder.setText(NbBundle.getMessage(CommandLineIngestSettingsPanel.class, "CommandLineIngestSettingsPanel.CannotAccess") + + " " + outputPath + " " + + NbBundle.getMessage(CommandLineIngestSettingsPanel.class, "CommandLineIngestSettingsPanel.CheckPermissions")); + return false; + } + + jLabelInvalidResultsFolder.setVisible(false); + return true; + } + + private void displayIngestJobSettingsPanel() { + this.getParent().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + + IngestJobSettings ingestJobSettings = new IngestJobSettings(org.sleuthkit.autopsy.commandlineingest.UserPreferences.getCommandLineModeIngestModuleContextString()); + showWarnings(ingestJobSettings); + IngestJobSettingsPanel ingestJobSettingsPanel = new IngestJobSettingsPanel(ingestJobSettings); + + add(ingestJobSettingsPanel, BorderLayout.PAGE_START); + + if (JOptionPane.showConfirmDialog(this, ingestJobSettingsPanel, "Ingest Module Configuration", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE) == JOptionPane.OK_OPTION) { + // store the updated settings + ingestJobSettings = ingestJobSettingsPanel.getSettings(); + ingestJobSettings.save(); + showWarnings(ingestJobSettings); + } + + this.getParent().setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + + private static void showWarnings(IngestJobSettings ingestJobSettings) { + List warnings = ingestJobSettings.getWarnings(); + if (warnings.isEmpty() == false) { + StringBuilder warningMessage = new StringBuilder(); + for (String warning : warnings) { + warningMessage.append(warning).append("\n"); + } + JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), warningMessage.toString()); + } + } + + /** + * 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() { + + nodeScrollPane = new javax.swing.JScrollPane(); + nodePanel = new javax.swing.JPanel(); + bnEditIngestSettings = new javax.swing.JButton(); + browseOutputFolderButton = new javax.swing.JButton(); + outputPathTextField = new javax.swing.JTextField(); + jLabelInvalidResultsFolder = new javax.swing.JLabel(); + jLabelSelectOutputFolder = new javax.swing.JLabel(); + + nodeScrollPane.setMinimumSize(new java.awt.Dimension(0, 0)); + + nodePanel.setMinimumSize(new java.awt.Dimension(100, 100)); + + org.openide.awt.Mnemonics.setLocalizedText(bnEditIngestSettings, org.openide.util.NbBundle.getMessage(CommandLineIngestSettingsPanel.class, "CommandLineIngestSettingsPanel.bnEditIngestSettings.text")); // NOI18N + bnEditIngestSettings.setToolTipText(org.openide.util.NbBundle.getMessage(CommandLineIngestSettingsPanel.class, "CommandLineIngestSettingsPanel.bnEditIngestSettings.toolTipText")); // NOI18N + bnEditIngestSettings.setActionCommand(org.openide.util.NbBundle.getMessage(CommandLineIngestSettingsPanel.class, "CommandLineIngestSettingsPanel.bnEditIngestSettings.text")); // NOI18N + bnEditIngestSettings.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnEditIngestSettingsActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(browseOutputFolderButton, org.openide.util.NbBundle.getMessage(CommandLineIngestSettingsPanel.class, "CommandLineIngestSettingsPanel.browseOutputFolderButton.text")); // NOI18N + browseOutputFolderButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + browseOutputFolderButtonActionPerformed(evt); + } + }); + + outputPathTextField.setText(org.openide.util.NbBundle.getMessage(CommandLineIngestSettingsPanel.class, "CommandLineIngestSettingsPanel.outputPathTextField.text")); // NOI18N + outputPathTextField.setToolTipText(org.openide.util.NbBundle.getMessage(CommandLineIngestSettingsPanel.class, "CommandLineIngestSettingsPanel.outputPathTextField.toolTipText")); // NOI18N + + jLabelInvalidResultsFolder.setForeground(new java.awt.Color(255, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(jLabelInvalidResultsFolder, org.openide.util.NbBundle.getMessage(CommandLineIngestSettingsPanel.class, "CommandLineIngestSettingsPanel.jLabelInvalidResultsFolder.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(jLabelSelectOutputFolder, org.openide.util.NbBundle.getMessage(CommandLineIngestSettingsPanel.class, "CommandLineIngestSettingsPanel.jLabelSelectOutputFolder.text")); // NOI18N + jLabelSelectOutputFolder.setVerticalAlignment(javax.swing.SwingConstants.BOTTOM); + + javax.swing.GroupLayout nodePanelLayout = new javax.swing.GroupLayout(nodePanel); + nodePanel.setLayout(nodePanelLayout); + nodePanelLayout.setHorizontalGroup( + nodePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(nodePanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(nodePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(nodePanelLayout.createSequentialGroup() + .addComponent(outputPathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 630, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(browseOutputFolderButton)) + .addComponent(bnEditIngestSettings, javax.swing.GroupLayout.PREFERRED_SIZE, 155, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(nodePanelLayout.createSequentialGroup() + .addComponent(jLabelSelectOutputFolder) + .addGap(18, 18, 18) + .addComponent(jLabelInvalidResultsFolder, javax.swing.GroupLayout.PREFERRED_SIZE, 544, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addContainerGap(355, Short.MAX_VALUE)) + ); + nodePanelLayout.setVerticalGroup( + nodePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(nodePanelLayout.createSequentialGroup() + .addGap(40, 40, 40) + .addGroup(nodePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabelSelectOutputFolder, javax.swing.GroupLayout.PREFERRED_SIZE, 21, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jLabelInvalidResultsFolder)) + .addGap(1, 1, 1) + .addGroup(nodePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(browseOutputFolderButton) + .addComponent(outputPathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGap(25, 25, 25) + .addComponent(bnEditIngestSettings) + .addContainerGap(389, Short.MAX_VALUE)) + ); + + browseOutputFolderButton.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(CommandLineIngestSettingsPanel.class, "CommandLineIngestSettingsPanel.browseOutputFolderButton.text")); // NOI18N + jLabelInvalidResultsFolder.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(CommandLineIngestSettingsPanel.class, "CommandLineIngestSettingsPanel.jLabelInvalidResultsFolder.text")); // NOI18N + jLabelSelectOutputFolder.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(CommandLineIngestSettingsPanel.class, "CommandLineIngestSettingsPanel.jLabelSelectOutputFolder.text")); // NOI18N + + nodeScrollPane.setViewportView(nodePanel); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(nodeScrollPane, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 864, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(nodeScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 421, Short.MAX_VALUE) + ); + }// //GEN-END:initComponents + + private void browseOutputFolderButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseOutputFolderButtonActionPerformed + String oldText = outputPathTextField.getText().trim(); + // set the current directory of the FileChooser if the oldText is valid + File currentDir = new File(oldText); + if (currentDir.exists()) { + fc.setCurrentDirectory(currentDir); + } + + fc.setDialogTitle("Select case output folder:"); + fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + + int retval = fc.showOpenDialog(this); + if (retval == JFileChooser.APPROVE_OPTION) { + String path = fc.getSelectedFile().getPath(); + outputPathTextField.setText(path); + valid(); + controller.changed(); + } + }//GEN-LAST:event_browseOutputFolderButtonActionPerformed + + private void bnEditIngestSettingsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnEditIngestSettingsActionPerformed + displayIngestJobSettingsPanel(); + }//GEN-LAST:event_bnEditIngestSettingsActionPerformed + + boolean permissionsAppropriate(String path) { + return FileUtil.hasReadWriteAccess(Paths.get(path)); + } + + private void resetUI() { + load(true); + controller.changed(); + } + + void setEnabledState(boolean enabled) { + bnEditIngestSettings.setEnabled(enabled); + browseOutputFolderButton.setEnabled(enabled); + jLabelInvalidResultsFolder.setEnabled(enabled); + jLabelSelectOutputFolder.setEnabled(enabled); + outputPathTextField.setEnabled(enabled); + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton bnEditIngestSettings; + private javax.swing.JButton browseOutputFolderButton; + private javax.swing.JLabel jLabelInvalidResultsFolder; + private javax.swing.JLabel jLabelSelectOutputFolder; + private javax.swing.JPanel nodePanel; + private javax.swing.JScrollPane nodeScrollPane; + private javax.swing.JTextField outputPathTextField; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestSettingsPanelController.java b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestSettingsPanelController.java new file mode 100755 index 0000000000..7856cdf6e0 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestSettingsPanelController.java @@ -0,0 +1,132 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.commandlineingest; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import javax.swing.JComponent; +import org.netbeans.spi.options.OptionsPanelController; +import org.openide.util.HelpCtx; +import org.openide.util.Lookup; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; + +@OptionsPanelController.TopLevelRegistration(categoryName = "#OptionsCategory_Name_Command_Line_Ingest", + iconBase = "org/sleuthkit/autopsy/images/command_line_icon.png", + position = 17, + keywords = "#OptionsCategory_Keywords_Command_Line_Ingest_Settings", + keywordsCategory = "Command Line Ingest") +public final class CommandLineIngestSettingsPanelController extends OptionsPanelController { + + private CommandLineIngestSettingsPanel panel; + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + private boolean changed; + private static final Logger logger = Logger.getLogger(CommandLineIngestSettingsPanelController.class.getName()); + + @Override + public void update() { + getPanel().load(false); + changed = false; + } + + @Override + public void applyChanges() { + getPanel().store(); + changed = false; + } + + @Override + public void cancel() { + } + + @Override + public boolean isValid() { + return getPanel().valid(); + } + + @Override + public boolean isChanged() { + return changed; + } + + @Override + public HelpCtx getHelpCtx() { + return null; + } + + @Override + public JComponent getComponent(Lookup masterLookup) { + return getPanel(); + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener l) { + if (pcs.getPropertyChangeListeners().length == 0) { + pcs.addPropertyChangeListener(l); + } + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener l) { + /** + * Note the NetBeans Framework does not appear to call this at all. We + * are using NetBeans 7.3.1 Build 201306052037. Perhaps in a future + * version of the Framework this will be resolved, but for now, simply + * don't unregister anything and add one time only in the + * addPropertyChangeListener() method above. + */ + } + + private CommandLineIngestSettingsPanel getPanel() { + if (panel == null) { + panel = new CommandLineIngestSettingsPanel(this); + panel.setSize(750, 600); //makes the panel large enough to hide the scroll bar + } + return panel; + } + + void changed() { + if (!changed) { + changed = true; + + try { + pcs.firePropertyChange(OptionsPanelController.PROP_CHANGED, false, true); + } catch (Exception e) { + logger.log(Level.SEVERE, "GeneralOptionsPanelController listener threw exception", e); //NON-NLS + MessageNotifyUtil.Notify.show( + NbBundle.getMessage(this.getClass(), "GeneralOptionsPanelController.moduleErr"), + NbBundle.getMessage(this.getClass(), "GeneralOptionsPanelController.moduleErr.msg"), + MessageNotifyUtil.MessageType.ERROR); + } + } + + try { + pcs.firePropertyChange(OptionsPanelController.PROP_VALID, null, null); + } catch (Exception e) { + logger.log(Level.SEVERE, "GeneralOptionsPanelController listener threw exception", e); //NON-NLS + MessageNotifyUtil.Notify.show( + NbBundle.getMessage(this.getClass(), "GeneralOptionsPanelController.moduleErr"), + NbBundle.getMessage(this.getClass(), "GeneralOptionsPanelController.moduleErr.msg"), + MessageNotifyUtil.MessageType.ERROR); + } + } +} + diff --git a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineOptionProcessor.java b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineOptionProcessor.java new file mode 100755 index 0000000000..36249ba497 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineOptionProcessor.java @@ -0,0 +1,168 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.commandlineingest; + +import java.io.File; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.netbeans.api.sendopts.CommandException; +import org.netbeans.spi.sendopts.Env; +import org.netbeans.spi.sendopts.Option; +import org.netbeans.spi.sendopts.OptionProcessor; +import org.openide.util.lookup.ServiceProvider; + +/** + * This class can be used to add command line options to Autopsy + */ +@ServiceProvider(service = OptionProcessor.class) +public class CommandLineOptionProcessor extends OptionProcessor { + + private static final Logger logger = Logger.getLogger(CommandLineOptionProcessor.class.getName()); + private final Option pathToDataSourceOption = Option.optionalArgument('l', "inputPath"); + private final Option caseNameOption = Option.optionalArgument('2', "caseName"); + private final Option runFromCommandLineOption = Option.optionalArgument('3', "runFromCommandLine"); + private String pathToDataSource; + private String baseCaseName; + private boolean runFromCommandLine = false; + + @Override + protected Set