19package org.sleuthkit.autopsy.modules.photoreccarver;
22import java.io.IOException;
23import java.lang.ProcessBuilder.Redirect;
24import java.nio.file.DirectoryStream;
25import java.nio.file.FileAlreadyExistsException;
26import java.nio.file.Files;
27import java.nio.file.Path;
28import java.nio.file.Paths;
29import java.text.DateFormat;
30import java.text.SimpleDateFormat;
31import java.util.ArrayList;
32import java.util.Arrays;
34import java.util.HashMap;
35import java.util.HashSet;
39import java.util.concurrent.ConcurrentHashMap;
40import java.util.concurrent.atomic.AtomicLong;
41import java.util.logging.Level;
42import java.util.stream.Collectors;
43import org.openide.modules.InstalledFileLocator;
44import org.openide.util.NbBundle;
45import org.sleuthkit.autopsy.casemodule.Case;
46import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
47import org.sleuthkit.autopsy.coreutils.ExecUtil;
48import org.sleuthkit.autopsy.coreutils.FileUtil;
49import org.sleuthkit.autopsy.coreutils.Logger;
50import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
51import org.sleuthkit.autopsy.coreutils.PlatformUtil;
52import org.sleuthkit.autopsy.coreutils.UNCPathUtilities;
53import org.sleuthkit.autopsy.datamodel.ContentUtils;
54import org.sleuthkit.autopsy.ingest.FileIngestModule;
55import org.sleuthkit.autopsy.ingest.FileIngestModuleProcessTerminator;
56import org.sleuthkit.autopsy.ingest.IngestJobContext;
57import org.sleuthkit.autopsy.ingest.IngestMessage;
58import org.sleuthkit.autopsy.ingest.IngestModule;
59import org.sleuthkit.autopsy.ingest.IngestModuleReferenceCounter;
60import org.sleuthkit.autopsy.ingest.IngestMonitor;
61import org.sleuthkit.autopsy.ingest.IngestServices;
62import org.sleuthkit.autopsy.ingest.ModuleContentEvent;
63import org.sleuthkit.autopsy.ingest.ProcTerminationCode;
64import org.sleuthkit.datamodel.AbstractFile;
65import org.sleuthkit.datamodel.Content;
66import org.sleuthkit.datamodel.DataSource;
67import org.sleuthkit.datamodel.LayoutFile;
68import org.sleuthkit.datamodel.ReadContentInputStream.ReadContentInputStreamException;
69import org.sleuthkit.datamodel.TskCoreException;
70import org.sleuthkit.datamodel.TskData;
71import org.sleuthkit.datamodel.VirtualDirectory;
78 "PhotoRecIngestModule.PermissionsNotSufficient=Insufficient permissions accessing",
79 "PhotoRecIngestModule.PermissionsNotSufficientSeeReference=See 'Shared Drive Authentication' in Autopsy help.",
80 "# {0} - output directory name",
"cannotCreateOutputDir.message=Unable to create output directory: {0}.",
81 "unallocatedSpaceProcessingSettingsError.message=The selected file ingest filter ignores unallocated space. This module carves unallocated space. Please choose a filter which does not ignore unallocated space or disable this module.",
82 "unsupportedOS.message=PhotoRec module is supported on Windows platforms only.",
83 "missingExecutable.message=Unable to locate PhotoRec executable.",
84 "cannotRunExecutable.message=Unable to execute PhotoRec.",
85 "PhotoRecIngestModule.nonHostnameUNCPathUsed=PhotoRec cannot operate with a UNC path containing IP addresses."
89 static final boolean DEFAULT_CONFIG_KEEP_CORRUPTED_FILES =
false;
91 = PhotoRecCarverIngestJobSettings.ExtensionFilterOption.
NO_FILTER;
93 static final boolean DEFAULT_CONFIG_INCLUDE_ELSE_EXCLUDE =
false;
95 private static final String PHOTOREC_TEMP_SUBDIR =
"PhotoRec Carver";
96 private static final String PHOTOREC_DIRECTORY =
"photorec_exec";
97 private static final String PHOTOREC_SUBDIRECTORY =
"bin";
98 private static final String PHOTOREC_EXECUTABLE =
"photorec_win.exe";
99 private static final String PHOTOREC_LINUX_EXECUTABLE =
"photorec";
100 private static final String PHOTOREC_RESULTS_BASE =
"results";
101 private static final String PHOTOREC_RESULTS_EXTENDED =
"results.1";
102 private static final String PHOTOREC_REPORT =
"report.xml";
103 private static final String LOG_FILE =
"run_log.txt";
104 private static final String SEP = System.getProperty(
"line.separator");
105 private static final Logger logger =
Logger.
getLogger(PhotoRecCarverFileIngestModule.class.getName());
106 private static final HashMap<Long, IngestJobTotals> totalsForIngestJobs =
new HashMap<>();
108 private static final Map<Long, WorkingPaths> pathsByJob =
new ConcurrentHashMap<>();
110 private Path rootOutputDirPath;
111 private Path rootTempDirPath;
112 private File executableFile;
115 private final PhotoRecCarverIngestJobSettings settings;
116 private String optionsString;
132 PhotoRecCarverFileIngestModule(PhotoRecCarverIngestJobSettings settings) {
133 this.settings = settings;
144 private String getPhotorecOptions(PhotoRecCarverIngestJobSettings settings) {
145 List<String> toRet =
new ArrayList<String>();
147 if (settings.isKeepCorruptedFiles()) {
148 toRet.addAll(Arrays.asList(
"options",
"keep_corrupted_file"));
151 if (settings.getExtensionFilterOption()
152 != PhotoRecCarverIngestJobSettings.ExtensionFilterOption.
NO_FILTER) {
155 toRet.add(
"fileopt");
157 String enable =
"enable";
158 String disable =
"disable";
162 String everythingEnable = settings.getExtensionFilterOption()
163 == PhotoRecCarverIngestJobSettings.ExtensionFilterOption.INCLUDE
166 toRet.addAll(Arrays.asList(
"everything", everythingEnable));
168 final String itemEnable = settings.getExtensionFilterOption()
169 == PhotoRecCarverIngestJobSettings.ExtensionFilterOption.INCLUDE
172 settings.getExtensions().forEach((extension) -> {
173 toRet.addAll(Arrays.asList(extension, itemEnable));
178 return String.join(
",", toRet);
181 private static synchronized IngestJobTotals getTotalsForIngestJobs(
long ingestJobId) {
182 IngestJobTotals totals = totalsForIngestJobs.get(ingestJobId);
183 if (totals ==
null) {
184 totals =
new PhotoRecCarverFileIngestModule.IngestJobTotals();
185 totalsForIngestJobs.put(ingestJobId, totals);
190 private static synchronized void initTotalsForIngestJob(
long ingestJobId) {
191 IngestJobTotals totals =
new PhotoRecCarverFileIngestModule.IngestJobTotals();
192 totalsForIngestJobs.put(ingestJobId, totals);
200 "# {0} - extensions",
201 "PhotoRecCarverFileIngestModule_startUp_invalidExtensions_description=The following extensions are invalid: {0}",
202 "PhotoRecCarverFileIngestModule_startUp_noExtensionsProvided_description=No extensions provided for PhotoRec to carve."
206 if (this.settings.getExtensionFilterOption() != PhotoRecCarverIngestJobSettings.ExtensionFilterOption.
NO_FILTER) {
207 if (this.settings.getExtensions().isEmpty()
208 &&
this.settings.getExtensionFilterOption() == PhotoRecCarverIngestJobSettings.ExtensionFilterOption.INCLUDE) {
211 Bundle.PhotoRecCarverFileIngestModule_startUp_noExtensionsProvided_description());
214 List<String> invalidExtensions = this.settings.getExtensions().stream()
215 .filter((ext) -> !PhotoRecCarverFileOptExtensions.isValidExtension(ext))
216 .collect(Collectors.toList());
218 if (!invalidExtensions.isEmpty()) {
220 Bundle.PhotoRecCarverFileIngestModule_startUp_invalidExtensions_description(
221 String.join(
",", invalidExtensions)));
225 this.optionsString = getPhotorecOptions(this.settings);
227 this.context = context;
229 this.jobId = this.context.getJobId();
235 if (!this.context.processingUnallocatedSpace()) {
239 this.rootOutputDirPath = createModuleOutputDirectoryForCase();
240 this.rootTempDirPath = createTempOutputDirectoryForCase();
243 executableFile = locateExecutable();
245 if (PhotoRecCarverFileIngestModule.refCounter.incrementAndGet(
this.jobId) == 1) {
248 DateFormat dateFormat =
new SimpleDateFormat(
"MM-dd-yyyy-HH-mm-ss-SSSS");
249 Date date =
new Date();
250 String folder = this.context.getDataSource().getId() +
"_" + dateFormat.format(date);
251 Path outputDirPath = Paths.get(this.rootOutputDirPath.toAbsolutePath().toString(), folder);
252 Files.createDirectories(outputDirPath);
255 Path tempDirPath = Paths.get(this.rootTempDirPath.toString(), folder);
256 Files.createDirectory(tempDirPath);
259 PhotoRecCarverFileIngestModule.pathsByJob.put(this.jobId,
new WorkingPaths(outputDirPath, tempDirPath));
262 initTotalsForIngestJob(jobId);
263 }
catch (SecurityException | IOException | UnsupportedOperationException ex) {
275 if (file.getType() != TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS) {
280 IngestJobTotals totals = getTotalsForIngestJobs(jobId);
282 Path tempFilePath =
null;
285 if (
null == this.executableFile) {
286 logger.log(Level.SEVERE,
"PhotoRec carver called after failed start up");
295 logger.log(Level.SEVERE,
"PhotoRec error processing {0} with {1} Not enough space on primary disk to save unallocated space.",
296 new Object[]{file.getName(), PhotoRecCarverIngestModuleFactory.getModuleName()});
298 NbBundle.getMessage(
this.getClass(),
"PhotoRecIngestModule.NotEnoughDiskSpace"));
301 if (this.context.fileIngestIsCancelled() ==
true) {
303 logger.log(Level.INFO,
"PhotoRec cancelled by user");
309 long writestart = System.currentTimeMillis();
310 WorkingPaths paths = PhotoRecCarverFileIngestModule.pathsByJob.get(this.jobId);
311 tempFilePath = Paths.get(paths.getTempDirPath().toString(), file.getName());
314 if (this.context.fileIngestIsCancelled() ==
true) {
316 logger.log(Level.INFO,
"PhotoRec cancelled by user");
322 Path outputDirPath = Paths.get(paths.getOutputDirPath().toString(), file.getName());
323 Files.createDirectory(outputDirPath);
324 File log =
new File(Paths.get(outputDirPath.toString(), LOG_FILE).toString());
327 ProcessBuilder processAndSettings =
new ProcessBuilder(
328 executableFile.toString(),
330 outputDirPath.toAbsolutePath().toString() + File.separator + PHOTOREC_RESULTS_BASE,
332 tempFilePath.toFile().toString());
334 processAndSettings.command().add(this.optionsString);
337 processAndSettings.environment().put(
"__COMPAT_LAYER",
"RunAsInvoker");
338 processAndSettings.redirectErrorStream(
true);
339 processAndSettings.redirectOutput(Redirect.appendTo(log));
344 if (this.context.fileIngestIsCancelled() ==
true) {
346 cleanup(outputDirPath, tempFilePath);
347 logger.log(Level.INFO,
"PhotoRec cancelled by user");
351 cleanup(outputDirPath, tempFilePath);
352 String msg = NbBundle.getMessage(this.getClass(),
"PhotoRecIngestModule.processTerminated") + file.getName();
354 logger.log(Level.SEVERE, msg);
356 }
else if (0 != exitValue) {
358 cleanup(outputDirPath, tempFilePath);
359 totals.totalItemsWithErrors.incrementAndGet();
360 logger.log(Level.SEVERE,
"PhotoRec carver returned error exit value = {0} when scanning {1}",
361 new Object[]{exitValue, file.getName()});
363 new Object[]{exitValue, file.getName()}));
368 java.io.File oldAuditFile =
new java.io.File(Paths.get(outputDirPath.toString(), PHOTOREC_RESULTS_EXTENDED, PHOTOREC_REPORT).toString());
369 java.io.File newAuditFile =
new java.io.File(Paths.get(outputDirPath.toString(), PHOTOREC_REPORT).toString());
370 oldAuditFile.renameTo(newAuditFile);
372 if (this.context.fileIngestIsCancelled() ==
true) {
374 logger.log(Level.INFO,
"PhotoRec cancelled by user");
378 Path pathToRemove = Paths.get(outputDirPath.toAbsolutePath().toString());
379 try (DirectoryStream<Path> stream = Files.newDirectoryStream(pathToRemove)) {
380 for (Path entry : stream) {
381 if (Files.isDirectory(entry)) {
386 long writedelta = (System.currentTimeMillis() - writestart);
387 totals.totalWritetime.addAndGet(writedelta);
390 long calcstart = System.currentTimeMillis();
391 PhotoRecCarverOutputParser parser =
new PhotoRecCarverOutputParser(outputDirPath);
392 if (this.context.fileIngestIsCancelled() ==
true) {
394 logger.log(Level.INFO,
"PhotoRec cancelled by user");
398 List<LayoutFile> carvedItems = parser.parse(newAuditFile, file, context);
399 long calcdelta = (System.currentTimeMillis() - calcstart);
400 totals.totalParsetime.addAndGet(calcdelta);
401 if (carvedItems !=
null && !carvedItems.isEmpty()) {
402 totals.totalItemsRecovered.addAndGet(carvedItems.size());
403 context.addFilesToJob(
new ArrayList<>(carvedItems));
407 List<AbstractFile> virtualParentDirs = getVirtualDirectoryParents(carvedItems);
408 for (AbstractFile virtualDir : virtualParentDirs) {
411 }
catch (TskCoreException ex) {
412 logger.log(Level.WARNING,
"Error collecting carved file parent directories", ex);
416 }
catch (ReadContentInputStreamException ex) {
417 totals.totalItemsWithErrors.incrementAndGet();
418 logger.log(Level.WARNING, String.format(
"Error reading file '%s' (id=%d) with the PhotoRec carver.", file.getName(), file.getId()), ex);
421 }
catch (IOException ex) {
422 totals.totalItemsWithErrors.incrementAndGet();
423 logger.log(Level.SEVERE, String.format(
"Error writing or processing file '%s' (id=%d) to '%s' with the PhotoRec carver.", file.getName(), file.getId(), tempFilePath), ex);
427 if (
null != tempFilePath && Files.exists(tempFilePath)) {
429 tempFilePath.toFile().delete();
448 private List<AbstractFile> getVirtualDirectoryParents(List<LayoutFile> layoutFiles)
throws TskCoreException {
450 Set<Long> processedParentIds =
new HashSet<>();
456 List<AbstractFile> parentFiles =
new ArrayList<>();
457 for (LayoutFile file : layoutFiles) {
458 AbstractFile currentFile = file;
459 while (currentFile.getParentId().isPresent() && !processedParentIds.contains(currentFile.getParentId().get())) {
460 Content parent = currentFile.getParent();
461 processedParentIds.add(parent.getId());
462 if (! (parent instanceof VirtualDirectory)
463 || (currentFile instanceof DataSource)) {
469 currentFile = (AbstractFile)parent;
470 parentFiles.add(currentFile);
476 private void cleanup(Path outputDirPath, Path tempFilePath) {
479 if (
null != tempFilePath && Files.exists(tempFilePath)) {
480 tempFilePath.toFile().delete();
484 private static synchronized void postSummary(
long jobId) {
485 IngestJobTotals jobTotals = totalsForIngestJobs.remove(jobId);
487 StringBuilder detailsSb =
new StringBuilder();
489 detailsSb.append(
"<table border='0' cellpadding='4' width='280'>");
491 detailsSb.append(
"<tr><td>")
492 .append(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
"PhotoRecIngestModule.complete.numberOfCarved"))
494 detailsSb.append(
"<td>").append(jobTotals.totalItemsRecovered.get()).append(
"</td></tr>");
496 detailsSb.append(
"<tr><td>")
497 .append(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
"PhotoRecIngestModule.complete.numberOfErrors"))
499 detailsSb.append(
"<td>").append(jobTotals.totalItemsWithErrors.get()).append(
"</td></tr>");
501 detailsSb.append(
"<tr><td>")
502 .append(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
"PhotoRecIngestModule.complete.totalWritetime"))
503 .append(
"</td><td>").append(jobTotals.totalWritetime.get()).append(
"</td></tr>\n");
504 detailsSb.append(
"<tr><td>")
505 .append(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
"PhotoRecIngestModule.complete.totalParsetime"))
506 .append(
"</td><td>").append(jobTotals.totalParsetime.get()).append(
"</td></tr>\n");
507 detailsSb.append(
"</table>");
512 NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
513 "PhotoRecIngestModule.complete.photoRecResults"),
514 detailsSb.toString()));
522 public void shutDown() {
523 if (this.context !=
null && refCounter.
decrementAndGet(
this.jobId) == 0) {
527 WorkingPaths paths = PhotoRecCarverFileIngestModule.pathsByJob.remove(this.jobId);
530 }
catch (SecurityException ex) {
531 logger.log(Level.SEVERE,
"Error shutting down PhotoRec carver module", ex);
536 private static final class WorkingPaths {
546 Path getOutputDirPath() {
547 return this.outputDirPath;
550 Path getTempDirPath() {
551 return this.tempDirPath;
563 synchronized Path createTempOutputDirectoryForCase() throws IngestModule.IngestModuleException {
565 Path path = Paths.get(Case.getCurrentCaseThrows().getTempDirectory(), PHOTOREC_TEMP_SUBDIR);
566 return createOutputDirectoryForCase(path);
567 }
catch (NoCurrentCaseException ex) {
568 throw new IngestModule.IngestModuleException(Bundle.cannotCreateOutputDir_message(ex.getLocalizedMessage()), ex);
580 synchronized Path createModuleOutputDirectoryForCase() throws IngestModule.IngestModuleException {
582 Path path = Paths.get(Case.getCurrentCaseThrows().getModuleDirectory(), PhotoRecCarverIngestModuleFactory.getModuleName());
583 return createOutputDirectoryForCase(path);
600 Path path = providedPath;
602 Files.createDirectory(path);
611 Bundle.PhotoRecIngestModule_PermissionsNotSufficient() + SEP + path.toString() + SEP
612 + Bundle.PhotoRecIngestModule_PermissionsNotSufficientSeeReference()
616 }
catch (FileAlreadyExistsException ex) {
618 }
catch (IOException | SecurityException | UnsupportedOperationException ex) {
633 public static File locateExecutable() throws
IngestModule.IngestModuleException {
636 Path execName = Paths.get(PHOTOREC_DIRECTORY, PHOTOREC_SUBDIRECTORY, PHOTOREC_EXECUTABLE);
637 exeFile = InstalledFileLocator.getDefault().locate(execName.toString(), PhotoRecCarverFileIngestModule.class.getPackage().getName(),
false);
640 for (String dirName: System.getenv(
"PATH").split(File.pathSeparator)) {
641 File testExe =
new File(dirName, PHOTOREC_LINUX_EXECUTABLE);
642 if (testExe.exists()) {
649 if (
null == exeFile) {
653 if (!exeFile.canExecute()) {
static int execute(ProcessBuilder processBuilder)
static boolean deleteDir(File dirPath)
static boolean hasReadWriteAccess(Path dirPath)
synchronized static Logger getLogger(String name)
static void error(String title, String message)
static void info(String title, String message)
synchronized static boolean isUNC(Path inputPath)
synchronized Path ipToHostName(Path inputPath)
static< T > long writeToFile(Content content, java.io.File outputFile, ProgressHandle progress, Future< T > worker, boolean source)
static IngestMessage createMessage(MessageType messageType, String source, String subject, String detailsHtml)
synchronized long decrementAndGet(long jobId)
static final int DISK_FREE_SPACE_UNKNOWN
void fireModuleContentEvent(ModuleContentEvent moduleContentEvent)
void postMessage(final IngestMessage message)
static synchronized IngestServices getInstance()
final AtomicLong totalItemsWithErrors
final AtomicLong totalWritetime
final AtomicLong totalItemsRecovered
final AtomicLong totalParsetime