Autopsy  4.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
PhotoRecCarverFileIngestModule.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2014 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.modules.photoreccarver;
20 
21 import java.io.File;
22 import java.io.IOException;
23 import java.lang.ProcessBuilder.Redirect;
24 import java.nio.file.DirectoryStream;
25 import java.nio.file.FileAlreadyExistsException;
26 import java.nio.file.Files;
27 import java.nio.file.Path;
28 import java.nio.file.Paths;
29 import java.text.DateFormat;
30 import java.text.SimpleDateFormat;
31 import java.util.ArrayList;
32 import java.util.Date;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.HashMap;
36 import java.util.concurrent.ConcurrentHashMap;
37 import java.util.concurrent.atomic.AtomicLong;
38 import java.util.logging.Level;
39 import org.openide.modules.InstalledFileLocator;
40 import org.openide.util.NbBundle;
50 import org.sleuthkit.datamodel.AbstractFile;
51 import org.sleuthkit.datamodel.Content;
52 import org.sleuthkit.datamodel.Image;
53 import org.sleuthkit.datamodel.LayoutFile;
54 import org.sleuthkit.datamodel.TskCoreException;
55 import org.sleuthkit.datamodel.TskData;
56 import org.sleuthkit.datamodel.Volume;
66 
71 final class PhotoRecCarverFileIngestModule implements FileIngestModule {
72 
73  private static final String PHOTOREC_DIRECTORY = "photorec_exec"; //NON-NLS
74  private static final String PHOTOREC_EXECUTABLE = "photorec_win.exe"; //NON-NLS
75  private static final String PHOTOREC_RESULTS_BASE = "results"; //NON-NLS
76  private static final String PHOTOREC_RESULTS_EXTENDED = "results.1"; //NON-NLS
77  private static final String PHOTOREC_REPORT = "report.xml"; //NON-NLS
78  private static final String LOG_FILE = "run_log.txt"; //NON-NLS
79  private static final String TEMP_DIR_NAME = "temp"; // NON-NLS
80  private static final String SEP = System.getProperty("line.separator");
81  private static final Logger logger = Logger.getLogger(PhotoRecCarverFileIngestModule.class.getName());
82  private static final HashMap<Long, IngestJobTotals> totalsForIngestJobs = new HashMap<>();
83  private static final IngestModuleReferenceCounter refCounter = new IngestModuleReferenceCounter();
84  private static final Map<Long, WorkingPaths> pathsByJob = new ConcurrentHashMap<>();
85  private IngestJobContext context;
86  private Path rootOutputDirPath;
87  private File executableFile;
88  private IngestServices services;
89  private UNCPathUtilities uncPathUtilities = new UNCPathUtilities();
90  private long jobId;
91 
92  private static class IngestJobTotals {
93 
94  private AtomicLong totalItemsRecovered = new AtomicLong(0);
95  private AtomicLong totalItemsWithErrors = new AtomicLong(0);
96  private AtomicLong totalWritetime = new AtomicLong(0);
97  private AtomicLong totalParsetime = new AtomicLong(0);
98  }
99 
100  private static synchronized IngestJobTotals getTotalsForIngestJobs(long ingestJobId) {
101  IngestJobTotals totals = totalsForIngestJobs.get(ingestJobId);
102  if (totals == null) {
103  totals = new PhotoRecCarverFileIngestModule.IngestJobTotals();
104  totalsForIngestJobs.put(ingestJobId, totals);
105  }
106  return totals;
107  }
108 
109  private static synchronized void initTotalsForIngestJob(long ingestJobId) {
110  IngestJobTotals totals = new PhotoRecCarverFileIngestModule.IngestJobTotals();
111  totalsForIngestJobs.put(ingestJobId, totals);
112  }
113 
117  @Override
118  public void startUp(IngestJobContext context) throws IngestModule.IngestModuleException {
119  this.context = context;
120  this.services = IngestServices.getInstance();
121  this.jobId = this.context.getJobId();
122 
123  // If the global unallocated space processing setting and the module
124  // process unallocated space only setting are not in sych, throw an
125  // exception. Although the result would not be incorrect, it would be
126  // unfortunate for the user to get an accidental no-op for this module.
127  if (!this.context.processingUnallocatedSpace()) {
128  throw new IngestModule.IngestModuleException(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "unallocatedSpaceProcessingSettingsError.message"));
129  }
130 
131  this.rootOutputDirPath = createModuleOutputDirectoryForCase();
132 
133  Path execName = Paths.get(PHOTOREC_DIRECTORY, PHOTOREC_EXECUTABLE);
134  executableFile = locateExecutable(execName.toString());
135 
136  if (PhotoRecCarverFileIngestModule.refCounter.incrementAndGet(this.jobId) == 1) {
137  try {
138  // The first instance creates an output subdirectory with a date and time stamp
139  DateFormat dateFormat = new SimpleDateFormat("MM-dd-yyyy-HH-mm-ss-SSSS"); // NON-NLS
140  Date date = new Date();
141  String folder = this.context.getDataSource().getId() + "_" + dateFormat.format(date);
142  Path outputDirPath = Paths.get(this.rootOutputDirPath.toAbsolutePath().toString(), folder);
143  Files.createDirectories(outputDirPath);
144 
145  // A temp subdirectory is also created as a location for writing unallocated space files to disk.
146  Path tempDirPath = Paths.get(outputDirPath.toString(), PhotoRecCarverFileIngestModule.TEMP_DIR_NAME);
147  Files.createDirectory(tempDirPath);
148 
149  // Save the directories for the current job.
150  PhotoRecCarverFileIngestModule.pathsByJob.put(this.jobId, new WorkingPaths(outputDirPath, tempDirPath));
151 
152  // Initialize job totals
153  initTotalsForIngestJob(jobId);
154  } catch (SecurityException | IOException | UnsupportedOperationException ex) {
155  throw new IngestModule.IngestModuleException(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "cannotCreateOutputDir.message", ex.getLocalizedMessage()), ex);
156  }
157  }
158  }
159 
163  @Override
164  public IngestModule.ProcessResult process(AbstractFile file) {
165  // Skip everything except unallocated space files.
166  if (file.getType() != TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS) {
167  return IngestModule.ProcessResult.OK;
168  }
169 
170  // Safely get a reference to the totalsForIngestJobs object
171  IngestJobTotals totals = getTotalsForIngestJobs(jobId);
172 
173  Path tempFilePath = null;
174  try {
175  long id = getRootId(file);
176  // make sure we have a valid systemID
177  if (id == -1) {
178  return ProcessResult.ERROR;
179  }
180 
181  // Verify initialization succeeded.
182  if (null == this.executableFile) {
183  logger.log(Level.SEVERE, "PhotoRec carver called after failed start up"); // NON-NLS
184  return IngestModule.ProcessResult.ERROR;
185  }
186 
187  // Check that we have roughly enough disk space left to complete the operation
188  // Some network drives always return -1 for free disk space.
189  // In this case, expect enough space and move on.
190  long freeDiskSpace = IngestServices.getInstance().getFreeDiskSpace();
191  if ((freeDiskSpace != IngestMonitor.DISK_FREE_SPACE_UNKNOWN) && ((file.getSize() * 1.2) > freeDiskSpace)) {
192  logger.log(Level.SEVERE, "PhotoRec error processing {0} with {1} Not enough space on primary disk to save unallocated space.", // NON-NLS
193  new Object[]{file.getName(), PhotoRecCarverIngestModuleFactory.getModuleName()}); // NON-NLS
194  MessageNotifyUtil.Notify.error(NbBundle.getMessage(this.getClass(), "PhotoRecIngestModule.UnableToCarve", file.getName()),
195  NbBundle.getMessage(this.getClass(), "PhotoRecIngestModule.NotEnoughDiskSpace"));
196  return IngestModule.ProcessResult.ERROR;
197  }
198 
199  // Write the file to disk.
200  long writestart = System.currentTimeMillis();
201  WorkingPaths paths = PhotoRecCarverFileIngestModule.pathsByJob.get(this.jobId);
202  tempFilePath = Paths.get(paths.getTempDirPath().toString(), file.getName());
203  ContentUtils.writeToFile(file, tempFilePath.toFile());
204 
205  // Create a subdirectory for this file.
206  Path outputDirPath = Paths.get(paths.getOutputDirPath().toString(), file.getName());
207  Files.createDirectory(outputDirPath);
208  File log = new File(Paths.get(outputDirPath.toString(), LOG_FILE).toString()); //NON-NLS
209 
210  // Scan the file with Unallocated Carver.
211  ProcessBuilder processAndSettings = new ProcessBuilder(
212  "\"" + executableFile + "\"",
213  "/d", // NON-NLS
214  "\"" + outputDirPath.toAbsolutePath() + File.separator + PHOTOREC_RESULTS_BASE + "\"",
215  "/cmd", // NON-NLS
216  "\"" + tempFilePath.toFile() + "\"",
217  "search"); // NON-NLS
218 
219  // Add environment variable to force PhotoRec to run with the same permissions Autopsy uses
220  processAndSettings.environment().put("__COMPAT_LAYER", "RunAsInvoker"); //NON-NLS
221  processAndSettings.redirectErrorStream(true);
222  processAndSettings.redirectOutput(Redirect.appendTo(log));
223 
224  FileIngestModuleProcessTerminator terminator = new FileIngestModuleProcessTerminator(this.context, true);
225  int exitValue = ExecUtil.execute(processAndSettings, terminator);
226 
227  if (this.context.fileIngestIsCancelled() == true) {
228  // if it was cancelled by the user, result is OK
229  cleanup(outputDirPath, tempFilePath);
230  logger.log(Level.INFO, "PhotoRec cancelled by user"); // NON-NLS
231  MessageNotifyUtil.Notify.info(PhotoRecCarverIngestModuleFactory.getModuleName(), NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.cancelledByUser"));
232  return IngestModule.ProcessResult.OK;
233  } else if (terminator.getTerminationCode() == ProcTerminationCode.TIME_OUT) {
234  cleanup(outputDirPath, tempFilePath);
235  String msg = NbBundle.getMessage(this.getClass(), "PhotoRecIngestModule.processTerminated") + file.getName(); // NON-NLS
236  MessageNotifyUtil.Notify.error(NbBundle.getMessage(this.getClass(), "PhotoRecIngestModule.moduleError"), msg); // NON-NLS
237  logger.log(Level.SEVERE, msg);
238  return IngestModule.ProcessResult.ERROR;
239  } else if (0 != exitValue) {
240  // if it failed or was cancelled by timeout, result is ERROR
241  cleanup(outputDirPath, tempFilePath);
242  totals.totalItemsWithErrors.incrementAndGet();
243  logger.log(Level.SEVERE, "PhotoRec carver returned error exit value = {0} when scanning {1}", // NON-NLS
244  new Object[]{exitValue, file.getName()}); // NON-NLS
245  MessageNotifyUtil.Notify.error(PhotoRecCarverIngestModuleFactory.getModuleName(), NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.error.exitValue", // NON-NLS
246  new Object[]{exitValue, file.getName()}));
247  return IngestModule.ProcessResult.ERROR;
248  }
249 
250  // Move carver log file to avoid placement into Autopsy results. PhotoRec appends ".1" to the folder name.
251  java.io.File oldAuditFile = new java.io.File(Paths.get(outputDirPath.toString(), PHOTOREC_RESULTS_EXTENDED, PHOTOREC_REPORT).toString()); //NON-NLS
252  java.io.File newAuditFile = new java.io.File(Paths.get(outputDirPath.toString(), PHOTOREC_REPORT).toString()); //NON-NLS
253  oldAuditFile.renameTo(newAuditFile);
254 
255  Path pathToRemove = Paths.get(outputDirPath.toAbsolutePath().toString());
256  try (DirectoryStream<Path> stream = Files.newDirectoryStream(pathToRemove)) {
257  for (Path entry : stream) {
258  if (Files.isDirectory(entry)) {
259  FileUtil.deleteDir(new File(entry.toString()));
260  }
261  }
262  }
263  long writedelta = (System.currentTimeMillis() - writestart);
264  totals.totalWritetime.addAndGet(writedelta);
265 
266  // Now that we've cleaned up the folders and data files, parse the xml output file to add carved items into the database
267  long calcstart = System.currentTimeMillis();
268  PhotoRecCarverOutputParser parser = new PhotoRecCarverOutputParser(outputDirPath);
269  List<LayoutFile> carvedItems = parser.parse(newAuditFile, id, file);
270  long calcdelta = (System.currentTimeMillis() - calcstart);
271  totals.totalParsetime.addAndGet(calcdelta);
272  if (carvedItems != null) { // if there were any results from carving, add the unallocated carving event to the reports list.
273  totals.totalItemsRecovered.addAndGet(carvedItems.size());
274  context.addFilesToJob(new ArrayList<>(carvedItems));
275  services.fireModuleContentEvent(new ModuleContentEvent(carvedItems.get(0))); // fire an event to update the tree
276  }
277  } catch (IOException ex) {
278  totals.totalItemsWithErrors.incrementAndGet();
279  logger.log(Level.SEVERE, "Error processing " + file.getName() + " with PhotoRec carver", ex); // NON-NLS
280  MessageNotifyUtil.Notify.error(PhotoRecCarverIngestModuleFactory.getModuleName(), NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.error.msg", file.getName()));
281  return IngestModule.ProcessResult.ERROR;
282  } finally {
283  if (null != tempFilePath && Files.exists(tempFilePath)) {
284  // Get rid of the unallocated space file.
285  tempFilePath.toFile().delete();
286  }
287  }
288  return IngestModule.ProcessResult.OK;
289 
290  }
291 
292  private void cleanup(Path outputDirPath, Path tempFilePath) {
293  // cleanup the output path
294  FileUtil.deleteDir(new File(outputDirPath.toString()));
295  if (null != tempFilePath && Files.exists(tempFilePath)) {
296  tempFilePath.toFile().delete();
297  }
298  }
299 
300  private static synchronized void postSummary(long jobId) {
301  IngestJobTotals jobTotals = totalsForIngestJobs.remove(jobId);
302 
303  StringBuilder detailsSb = new StringBuilder();
304  //details
305  detailsSb.append("<table border='0' cellpadding='4' width='280'>"); //NON-NLS
306 
307  detailsSb.append("<tr><td>") //NON-NLS
308  .append(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.complete.numberOfCarved"))
309  .append("</td>"); //NON-NLS
310  detailsSb.append("<td>").append(jobTotals.totalItemsRecovered.get()).append("</td></tr>"); //NON-NLS
311 
312  detailsSb.append("<tr><td>") //NON-NLS
313  .append(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.complete.numberOfErrors"))
314  .append("</td>"); //NON-NLS
315  detailsSb.append("<td>").append(jobTotals.totalItemsWithErrors.get()).append("</td></tr>"); //NON-NLS
316 
317  detailsSb.append("<tr><td>") //NON-NLS
318  .append(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.complete.totalWritetime"))
319  .append("</td><td>").append(jobTotals.totalWritetime.get()).append("</td></tr>\n"); //NON-NLS
320  detailsSb.append("<tr><td>") //NON-NLS
321  .append(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.complete.totalParsetime"))
322  .append("</td><td>").append(jobTotals.totalParsetime.get()).append("</td></tr>\n"); //NON-NLS
323  detailsSb.append("</table>"); //NON-NLS
324 
325  IngestServices.getInstance().postMessage(IngestMessage.createMessage(
326  IngestMessage.MessageType.INFO,
327  PhotoRecCarverIngestModuleFactory.getModuleName(),
328  NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
329  "PhotoRecIngestModule.complete.photoRecResults"),
330  detailsSb.toString()));
331 
332  }
333 
337  @Override
338  public void shutDown() {
339  if (this.context != null && refCounter.decrementAndGet(this.jobId) == 0) {
340  try {
341  // The last instance of this module for an ingest job cleans out
342  // the working paths map entry for the job and deletes the temp dir.
343  WorkingPaths paths = PhotoRecCarverFileIngestModule.pathsByJob.remove(this.jobId);
344  FileUtil.deleteDir(new File(paths.getTempDirPath().toString()));
345  postSummary(jobId);
346  } catch (SecurityException ex) {
347  logger.log(Level.SEVERE, "Error shutting down PhotoRec carver module", ex); // NON-NLS
348  }
349  }
350  }
351 
352  private static final class WorkingPaths {
353 
354  private final Path outputDirPath;
355  private final Path tempDirPath;
356 
357  WorkingPaths(Path outputDirPath, Path tempDirPath) {
358  this.outputDirPath = outputDirPath;
359  this.tempDirPath = tempDirPath;
360  }
361 
362  Path getOutputDirPath() {
363  return this.outputDirPath;
364  }
365 
366  Path getTempDirPath() {
367  return this.tempDirPath;
368  }
369  }
370 
379  synchronized Path createModuleOutputDirectoryForCase() throws IngestModule.IngestModuleException {
380  Path path = Paths.get(Case.getCurrentCase().getModuleDirectory(), PhotoRecCarverIngestModuleFactory.getModuleName());
381  try {
382  Files.createDirectory(path);
383  if (UNCPathUtilities.isUNC(path)) {
384  // if the UNC path is using an IP address, convert to hostname
385  path = uncPathUtilities.ipToHostName(path);
386  if (path == null) {
387  throw new IngestModule.IngestModuleException(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.nonHostnameUNCPathUsed"));
388  }
389  if (false == FileUtil.hasReadWriteAccess(path)) {
390  throw new IngestModule.IngestModuleException(
391  NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.PermissionsNotSufficient")
392  + SEP + path.toString() + SEP // SEP is line breaks to make the dialog display nicely.
393  + NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.PermissionsNotSufficientSeeReference"));
394  }
395  }
396  } catch (FileAlreadyExistsException ex) {
397  // No worries.
398  } catch (IOException | SecurityException | UnsupportedOperationException ex) {
399  throw new IngestModule.IngestModuleException(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "cannotCreateOutputDir.message", ex.getLocalizedMessage()), ex);
400  }
401  return path;
402  }
403 
411  private static long getRootId(AbstractFile file) {
412  long id = -1;
413  Content parent = null;
414  try {
415  parent = file.getParent();
416  while (parent != null) {
417  if (parent instanceof Volume || parent instanceof Image) {
418  id = parent.getId();
419  break;
420  }
421  parent = parent.getParent();
422  }
423  } catch (TskCoreException ex) {
424  logger.log(Level.SEVERE, "PhotoRec carver exception while trying to get parent of AbstractFile.", ex); //NON-NLS
425  }
426  return id;
427  }
428 
438  public static File locateExecutable(String executableToFindName) throws IngestModule.IngestModuleException {
439  // Must be running under a Windows operating system.
440  if (!PlatformUtil.isWindowsOS()) {
441  throw new IngestModule.IngestModuleException(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "unsupportedOS.message"));
442  }
443 
444  File exeFile = InstalledFileLocator.getDefault().locate(executableToFindName, PhotoRecCarverFileIngestModule.class.getPackage().getName(), false);
445  if (null == exeFile) {
446  throw new IngestModule.IngestModuleException(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "missingExecutable.message"));
447  }
448 
449  if (!exeFile.canExecute()) {
450  throw new IngestModule.IngestModuleException(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "cannotRunExecutable.message"));
451  }
452 
453  return exeFile;
454  }
455 
456 }
void fireModuleContentEvent(ModuleContentEvent moduleContentEvent)
synchronized static Logger getLogger(String name)
Definition: Logger.java:166
synchronized Path ipToHostName(Path inputPath)
static synchronized IngestServices getInstance()

Copyright © 2012-2015 Basis Technology. Generated on: Wed Apr 6 2016
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.