Autopsy  4.11.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
CommandLineIngestManager.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2019-2019 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.commandlineingest;
20 
21 import java.beans.PropertyChangeEvent;
22 import java.beans.PropertyChangeListener;
23 import java.io.File;
24 import java.io.FilenameFilter;
25 import java.nio.file.Path;
26 import java.nio.file.Paths;
27 import java.util.List;
28 import java.util.UUID;
29 import java.util.Collection;
30 import java.util.Iterator;
31 import java.util.logging.Level;
32 import org.netbeans.spi.sendopts.OptionProcessor;
33 import org.openide.LifecycleManager;
34 import org.openide.util.Lookup;
58 import org.sleuthkit.datamodel.Content;
59 
66 
67  private static final Logger LOGGER = Logger.getLogger(CommandLineIngestManager.class.getName());
68  private Path rootOutputDirectory;
69 
71  }
72 
73  public void start() {
74  new Thread(new JobProcessingTask()).start();
75  }
76 
77  public void stop() {
78  try {
79  // close current case if there is one open
81  } catch (CaseActionException ex) {
82  LOGGER.log(Level.WARNING, "Unable to close the case while shutting down command line ingest manager", ex); //NON-NLS
83  }
84 
85  // shut down Autopsy
86  LifecycleManager.getDefault().exit();
87  }
88 
89  private final class JobProcessingTask implements Runnable {
90 
91  private final Object ingestLock;
92 
93  private JobProcessingTask() {
94  ingestLock = new Object();
95  try {
97  LOGGER.log(Level.INFO, "Set running with desktop GUI runtime property to false");
99  LOGGER.log(Level.SEVERE, "Failed to set running with desktop GUI runtime property to false", ex);
100  }
101  }
102 
103  public void run() {
104  LOGGER.log(Level.INFO, "Job processing task started");
105 
106  try {
107  // read command line inputs
108  LOGGER.log(Level.INFO, "Autopsy is running from command line"); //NON-NLS
109  String dataSourcePath = "";
110  String baseCaseName = "";
111 
112  // first look up all OptionProcessors and get input data from CommandLineOptionProcessor
113  Collection<? extends OptionProcessor> optionProcessors = Lookup.getDefault().lookupAll(OptionProcessor.class);
114  Iterator<? extends OptionProcessor> optionsIterator = optionProcessors.iterator();
115  while (optionsIterator.hasNext()) {
116  // find CommandLineOptionProcessor
117  OptionProcessor processor = optionsIterator.next();
118  if (processor instanceof CommandLineOptionProcessor) {
119  // check if we are running from command line
120  dataSourcePath = ((CommandLineOptionProcessor) processor).getPathToDataSource();
121  baseCaseName = ((CommandLineOptionProcessor) processor).getBaseCaseName();
122  }
123  }
124 
125  LOGGER.log(Level.INFO, "Data source path = {0}", dataSourcePath); //NON-NLS
126  LOGGER.log(Level.INFO, "Case name = {0}", baseCaseName); //NON-NLS
127  System.out.println("Data source path = " + dataSourcePath);
128  System.out.println("Case name = " + baseCaseName);
129 
130  // verify inputs
131  if (dataSourcePath.isEmpty()) {
132  LOGGER.log(Level.SEVERE, "Data source path not specified");
133  System.out.println("Data source path not specified");
134  return;
135  }
136 
137  if (baseCaseName.isEmpty()) {
138  LOGGER.log(Level.SEVERE, "Case name not specified");
139  System.out.println("Case name not specified");
140  return;
141  }
142 
143  if (!(new File(dataSourcePath).exists())) {
144  LOGGER.log(Level.SEVERE, "Data source file not found {0}", dataSourcePath);
145  System.out.println("Data source file not found " + dataSourcePath);
146  return;
147  }
148 
149  // read options panel configuration
150  String rootOutputDir = UserPreferences.getCommandLineModeResultsFolder();
151  LOGGER.log(Level.INFO, "Output directory = {0}", rootOutputDir); //NON-NLS
152  System.out.println("Output directory = " + rootOutputDir);
153 
154  if (rootOutputDir.isEmpty()) {
155  LOGGER.log(Level.SEVERE, "Output directory not specified, please configure Command Line Options Panel (in Tools -> Options)");
156  System.out.println("Output directory not specified, please configure Command Line Options Panel (in Tools -> Options)");
157  return;
158  }
159 
160  if (!(new File(rootOutputDir).exists())) {
161  LOGGER.log(Level.SEVERE, "The output directory doesn't exist {0}", rootOutputDir);
162  System.out.println("The output directory doesn't exist " + rootOutputDir);
163  return;
164  }
165  rootOutputDirectory = Paths.get(rootOutputDir);
166 
167  // open case
168  Case caseForJob;
169  try {
170  caseForJob = openCase(baseCaseName);
171  } catch (CaseActionException ex) {
172  LOGGER.log(Level.SEVERE, "Error creating or opening case " + baseCaseName, ex);
173  System.out.println("Error creating or opening case " + baseCaseName);
174  return;
175  }
176 
177  if (caseForJob == null) {
178  LOGGER.log(Level.SEVERE, "Error creating or opening case {0}", baseCaseName);
179  System.out.println("Error creating or opening case " + baseCaseName);
180  return;
181  }
182 
183  AutoIngestDataSource dataSource = new AutoIngestDataSource("", Paths.get(dataSourcePath));
184  try {
185  // run data source processor
186  runDataSourceProcessor(caseForJob, dataSource);
187 
188  // run ingest manager
189  analyze(dataSource);
190 
191  // generate CASE-UCO report
192  Long selectedDataSourceId = getDataSourceId(dataSource);
193  Path reportFolderPath = Paths.get(caseForJob.getReportDirectory(), "CASE-UCO", "Data_Source_ID_" + selectedDataSourceId.toString() + "_" + TimeStampUtils.createTimeStamp(), ReportCaseUco.getReportFileName()); //NON_NLS
194  ReportProgressPanel progressPanel = new ReportProgressPanel("CASE_UCO", rootOutputDir); // dummy progress panel
195  CaseUcoFormatExporter.generateReport(selectedDataSourceId, reportFolderPath.toString(), progressPanel);
197  LOGGER.log(Level.SEVERE, "Unable to ingest data source " + dataSourcePath + ". Exiting...", ex);
198  System.out.println("Unable to ingest data source " + dataSourcePath + ". Exiting...");
199  } catch (Throwable ex) {
200  /*
201  * Unexpected runtime exceptions firewall. This task is designed to
202  * be able to be run in an executor service thread pool without
203  * calling get() on the task's Future<Void>, so this ensures that
204  * such errors get logged.
205  */
206  LOGGER.log(Level.SEVERE, "Unexpected error while ingesting data source " + dataSourcePath, ex);
207  System.out.println("Unexpected error while ingesting data source " + dataSourcePath + ". Exiting...");
208 
209  } finally {
210  try {
212  } catch (CaseActionException ex) {
213  LOGGER.log(Level.WARNING, "Exception while closing case", ex);
214  System.out.println("Exception while closing case");
215  }
216  }
217 
218  } finally {
219  LOGGER.log(Level.INFO, "Job processing task finished");
220  System.out.println("Job processing task finished");
221 
222  // shut down Autopsy
223  stop();
224  }
225  }
226 
234  private Long getDataSourceId(AutoIngestDataSource dataSource) {
235  Content content = dataSource.getContent().get(0);
236  return content.getId();
237  }
238 
239  private Case openCase(String baseCaseName) throws CaseActionException {
240 
241  LOGGER.log(Level.INFO, "Opening case {0}", baseCaseName);
242 
243  Path caseDirectoryPath = findCaseDirectory(rootOutputDirectory, baseCaseName);
244  if (null != caseDirectoryPath) {
245  // found an existing case directory for same case name. the input case name must be unique. Exit.
246  LOGGER.log(Level.SEVERE, "Case {0} already exists. Case name must be unique. Exiting", baseCaseName);
247  throw new CaseActionException("Case " + baseCaseName + " already exists. Case name must be unique. Exiting");
248  } else {
249  caseDirectoryPath = createCaseFolderPath(rootOutputDirectory, baseCaseName);
250 
251  // Create the case directory
252  Case.createCaseDirectory(caseDirectoryPath.toString(), Case.CaseType.SINGLE_USER_CASE);
253 
254  CaseDetails caseDetails = new CaseDetails(baseCaseName);
255  Case.createAsCurrentCase(Case.CaseType.SINGLE_USER_CASE, caseDirectoryPath.toString(), caseDetails);
256  }
257 
258  Case caseForJob = Case.getCurrentCase();
259  LOGGER.log(Level.INFO, "Opened case {0}", caseForJob.getName());
260  return caseForJob;
261  }
262 
279 
280  LOGGER.log(Level.INFO, "Adding data source {0} ", dataSource.getPath().toString());
281 
282  // Get an ordered list of data source processors to try
283  List<AutoIngestDataSourceProcessor> validDataSourceProcessors;
284  try {
285  validDataSourceProcessors = DataSourceProcessorUtility.getOrderedListOfDataSourceProcessors(dataSource.getPath());
287  LOGGER.log(Level.SEVERE, "Exception while determining best data source processor for {0}", dataSource.getPath());
288  // rethrow the exception.
289  throw ex;
290  }
291 
292  // did we find a data source processor that can process the data source
293  if (validDataSourceProcessors.isEmpty()) {
294  // This should never happen. We should add all unsupported data sources as logical files.
295  LOGGER.log(Level.SEVERE, "Unsupported data source {0}", dataSource.getPath()); // NON-NLS
296  return;
297  }
298 
300  synchronized (ingestLock) {
301  // Try each DSP in decreasing order of confidence
302  for (AutoIngestDataSourceProcessor selectedProcessor : validDataSourceProcessors) {
303  UUID taskId = UUID.randomUUID();
304  caseForJob.notifyAddingDataSource(taskId);
305  DataSourceProcessorCallback callBack = new AddDataSourceCallback(caseForJob, dataSource, taskId, ingestLock);
306  caseForJob.notifyAddingDataSource(taskId);
307  LOGGER.log(Level.INFO, "Identified data source type for {0} as {1}", new Object[]{dataSource.getPath(), selectedProcessor.getDataSourceType()});
308  selectedProcessor.process(dataSource.getDeviceId(), dataSource.getPath(), progressMonitor, callBack);
309  ingestLock.wait();
310 
311  // at this point we got the content object(s) from the current DSP.
312  // check whether the data source was processed successfully
313  if ((dataSource.getResultDataSourceProcessorResultCode() == CRITICAL_ERRORS)
314  || dataSource.getContent().isEmpty()) {
315  // move onto the the next DSP that can process this data source
316  logDataSourceProcessorResult(dataSource);
317  continue;
318  }
319 
320  logDataSourceProcessorResult(dataSource);
321  return;
322  }
323  // If we get to this point, none of the processors were successful
324  LOGGER.log(Level.SEVERE, "All data source processors failed to process {0}", dataSource.getPath());
325  // Throw an exception. It will get caught & handled upstream and will result in AIM auto-pause.
326  throw new AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException("Failed to process " + dataSource.getPath() + " with all data source processors");
327  }
328  }
329 
337 
339  if (null != resultCode) {
340  switch (resultCode) {
341  case NO_ERRORS:
342  LOGGER.log(Level.INFO, "Added data source to case");
343  if (dataSource.getContent().isEmpty()) {
344  LOGGER.log(Level.SEVERE, "Data source failed to produce content");
345  }
346  break;
347 
348  case NONCRITICAL_ERRORS:
349  for (String errorMessage : dataSource.getDataSourceProcessorErrorMessages()) {
350  LOGGER.log(Level.WARNING, "Non-critical error running data source processor for {0}: {1}", new Object[]{dataSource.getPath(), errorMessage});
351  }
352  LOGGER.log(Level.INFO, "Added data source to case");
353  if (dataSource.getContent().isEmpty()) {
354  LOGGER.log(Level.SEVERE, "Data source failed to produce content");
355  }
356  break;
357 
358  case CRITICAL_ERRORS:
359  for (String errorMessage : dataSource.getDataSourceProcessorErrorMessages()) {
360  LOGGER.log(Level.SEVERE, "Critical error running data source processor for {0}: {1}", new Object[]{dataSource.getPath(), errorMessage});
361  }
362  LOGGER.log(Level.SEVERE, "Failed to add data source to case");
363  break;
364  }
365  } else {
366  LOGGER.log(Level.WARNING, "No result code for data source processor for {0}", dataSource.getPath());
367  }
368  }
369 
383  private void analyze(AutoIngestDataSource dataSource) throws AnalysisStartupException, InterruptedException {
384 
385  LOGGER.log(Level.INFO, "Starting ingest modules analysis for {0} ", dataSource.getPath());
386  IngestJobEventListener ingestJobEventListener = new IngestJobEventListener();
387  IngestManager.getInstance().addIngestJobEventListener(ingestJobEventListener);
388  try {
389  synchronized (ingestLock) {
391  List<String> settingsWarnings = ingestJobSettings.getWarnings();
392  if (settingsWarnings.isEmpty()) {
393  IngestJobStartResult ingestJobStartResult = IngestManager.getInstance().beginIngestJob(dataSource.getContent(), ingestJobSettings);
394  IngestJob ingestJob = ingestJobStartResult.getJob();
395  if (null != ingestJob) {
396  /*
397  * Block until notified by the ingest job event
398  * listener or until interrupted because auto ingest
399  * is shutting down.
400  */
401  ingestLock.wait();
402  LOGGER.log(Level.INFO, "Finished ingest modules analysis for {0} ", dataSource.getPath());
403  IngestJob.ProgressSnapshot jobSnapshot = ingestJob.getSnapshot();
404  for (IngestJob.ProgressSnapshot.DataSourceProcessingSnapshot snapshot : jobSnapshot.getDataSourceSnapshots()) {
405  if (!snapshot.isCancelled()) {
406  List<String> cancelledModules = snapshot.getCancelledDataSourceIngestModules();
407  if (!cancelledModules.isEmpty()) {
408  LOGGER.log(Level.WARNING, String.format("Ingest module(s) cancelled for %s", dataSource.getPath()));
409  for (String module : snapshot.getCancelledDataSourceIngestModules()) {
410  LOGGER.log(Level.WARNING, String.format("%s ingest module cancelled for %s", module, dataSource.getPath()));
411  }
412  }
413  LOGGER.log(Level.INFO, "Analysis of data source completed");
414  } else {
415  LOGGER.log(Level.WARNING, "Analysis of data source cancelled");
416  IngestJob.CancellationReason cancellationReason = snapshot.getCancellationReason();
417  if (IngestJob.CancellationReason.NOT_CANCELLED != cancellationReason && IngestJob.CancellationReason.USER_CANCELLED != cancellationReason) {
418  throw new AnalysisStartupException(String.format("Analysis cancelled due to %s for %s", cancellationReason.getDisplayName(), dataSource.getPath()));
419  }
420  }
421  }
422  } else if (!ingestJobStartResult.getModuleErrors().isEmpty()) {
423  for (IngestModuleError error : ingestJobStartResult.getModuleErrors()) {
424  LOGGER.log(Level.SEVERE, String.format("%s ingest module startup error for %s", error.getModuleDisplayName(), dataSource.getPath()), error.getThrowable());
425  }
426  LOGGER.log(Level.SEVERE, "Failed to analyze data source due to ingest job startup error");
427  throw new AnalysisStartupException(String.format("Error(s) during ingest module startup for %s", dataSource.getPath()));
428  } else {
429  LOGGER.log(Level.SEVERE, String.format("Ingest manager ingest job start error for %s", dataSource.getPath()), ingestJobStartResult.getStartupException());
430  throw new AnalysisStartupException("Ingest manager error starting job", ingestJobStartResult.getStartupException());
431  }
432  } else {
433  for (String warning : settingsWarnings) {
434  LOGGER.log(Level.SEVERE, "Ingest job settings error for {0}: {1}", new Object[]{dataSource.getPath(), warning});
435  }
436  LOGGER.log(Level.SEVERE, "Failed to analyze data source due to settings errors");
437  throw new AnalysisStartupException("Error(s) in ingest job settings");
438  }
439  }
440  } finally {
441  IngestManager.getInstance().removeIngestJobEventListener(ingestJobEventListener);
442  }
443  }
444 
454  Path createCaseFolderPath(Path caseFoldersPath, String caseName) {
455  String folderName = caseName + "_" + TimeStampUtils.createTimeStamp();
456  return Paths.get(caseFoldersPath.toString(), folderName);
457  }
458 
469  Path findCaseDirectory(Path folderToSearch, String caseName) {
470  File searchFolder = new File(folderToSearch.toString());
471  if (!searchFolder.isDirectory()) {
472  return null;
473  }
474  Path caseFolderPath = null;
475  String[] candidateFolders = searchFolder.list(new CaseFolderFilter(caseName));
476  long mostRecentModified = 0;
477  for (String candidateFolder : candidateFolders) {
478  File file = new File(candidateFolder);
479  if (file.lastModified() >= mostRecentModified) {
480  mostRecentModified = file.lastModified();
481  caseFolderPath = Paths.get(folderToSearch.toString(), file.getPath());
482  }
483  }
484  return caseFolderPath;
485  }
486 
496  private class IngestJobEventListener implements PropertyChangeListener {
497 
505  @Override
506  public void propertyChange(PropertyChangeEvent event) {
507  if (AutopsyEvent.SourceType.LOCAL == ((AutopsyEvent) event).getSourceType()) {
508  String eventType = event.getPropertyName();
509  if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) {
510  synchronized (ingestLock) {
511  ingestLock.notify();
512  }
513  }
514  }
515  }
516  };
517 
524 
530  @Override
531  public void setIndeterminate(final boolean indeterminate) {
532  }
533 
539  @Override
540  public void setProgress(final int progress) {
541  }
542 
548  @Override
549  public void setProgressText(final String text) {
550  }
551  }
552 
558  private final class AnalysisStartupException extends Exception {
559 
560  private static final long serialVersionUID = 1L;
561 
562  private AnalysisStartupException(String message) {
563  super(message);
564  }
565 
566  private AnalysisStartupException(String message, Throwable cause) {
567  super(message, cause);
568  }
569  }
570  }
571 
572  private static class CaseFolderFilter implements FilenameFilter {
573 
574  private final String caseName;
575  private final static String CASE_METADATA_EXT = CaseMetadata.getFileExtension();
576 
577  CaseFolderFilter(String caseName) {
578  this.caseName = caseName;
579  }
580 
581  @Override
582  public boolean accept(File folder, String fileName) {
583  File file = new File(folder, fileName);
584  if (fileName.length() > TimeStampUtils.getTimeStampLength() && file.isDirectory()) {
585  if (TimeStampUtils.endsWithTimeStamp(fileName)) {
586  if (null != caseName) {
587  String fileNamePrefix = fileName.substring(0, fileName.length() - TimeStampUtils.getTimeStampLength());
588  if (fileNamePrefix.equals(caseName)) {
589  return hasCaseMetadataFile(file);
590  }
591  } else {
592  return hasCaseMetadataFile(file);
593  }
594  }
595  }
596  return false;
597  }
598 
607  private static boolean hasCaseMetadataFile(File folder) {
608  for (File file : folder.listFiles()) {
609  if (file.getName().toLowerCase().endsWith(CASE_METADATA_EXT) && file.isFile()) {
610  return true;
611  }
612  }
613  return false;
614  }
615  }
616 
617 }
static List< AutoIngestDataSourceProcessor > getOrderedListOfDataSourceProcessors(Path dataSourcePath)
static synchronized IngestManager getInstance()
static void generateReport(Long selectedDataSourceId, String reportOutputPath, ReportProgressPanel progressPanel)
static void createCaseDirectory(String caseDirPath, CaseType caseType)
Definition: Case.java:848
IngestJobStartResult beginIngestJob(Collection< Content > dataSources, IngestJobSettings settings)
synchronized DataSourceProcessorResult getResultDataSourceProcessorResultCode()
static boolean endsWithTimeStamp(String inputString)
void removeIngestJobEventListener(final PropertyChangeListener listener)
void addIngestJobEventListener(final PropertyChangeListener listener)
static synchronized void setRunningWithGUI(boolean runningWithGUI)
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
static void createAsCurrentCase(String caseDir, String caseDisplayName, String caseNumber, String examiner, CaseType caseType)
Definition: Case.java:531
IngestManager.IngestManagerException getStartupException()

Copyright © 2012-2018 Basis Technology. Generated on: Fri Jun 21 2019
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.