19 package org.sleuthkit.autopsy.modules.photoreccarver;
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.HashMap;
34 import java.util.List;
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;
62 import org.
sleuthkit.datamodel.ReadContentInputStream.ReadContentInputStreamException;
70 "PhotoRecIngestModule.PermissionsNotSufficient=Insufficient permissions accessing",
71 "PhotoRecIngestModule.PermissionsNotSufficientSeeReference=See 'Shared Drive Authentication' in Autopsy help.",
72 "# {0} - output directory name",
"cannotCreateOutputDir.message=Unable to create output directory: {0}.",
73 "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.",
74 "unsupportedOS.message=PhotoRec module is supported on Windows platforms only.",
75 "missingExecutable.message=Unable to locate PhotoRec executable.",
76 "cannotRunExecutable.message=Unable to execute PhotoRec.",
77 "PhotoRecIngestModule.nonHostnameUNCPathUsed=PhotoRec cannot operate with a UNC path containing IP addresses."
81 static final boolean DEFAULT_CONFIG_KEEP_CORRUPTED_FILES =
false;
83 private static final String PHOTOREC_DIRECTORY =
"photorec_exec";
84 private static final String PHOTOREC_SUBDIRECTORY =
"bin";
85 private static final String PHOTOREC_EXECUTABLE =
"photorec_win.exe";
86 private static final String PHOTOREC_LINUX_EXECUTABLE =
"photorec";
87 private static final String PHOTOREC_RESULTS_BASE =
"results";
88 private static final String PHOTOREC_RESULTS_EXTENDED =
"results.1";
89 private static final String PHOTOREC_REPORT =
"report.xml";
90 private static final String LOG_FILE =
"run_log.txt";
91 private static final String TEMP_DIR_NAME =
"temp";
92 private static final String SEP = System.getProperty(
"line.separator");
93 private static final Logger logger =
Logger.
getLogger(PhotoRecCarverFileIngestModule.class.getName());
94 private static final HashMap<Long, IngestJobTotals> totalsForIngestJobs =
new HashMap<>();
96 private static final Map<Long, WorkingPaths> pathsByJob =
new ConcurrentHashMap<>();
98 private Path rootOutputDirPath;
99 private File executableFile;
104 private final boolean keepCorruptedFiles;
107 private final AtomicLong totalItemsRecovered =
new AtomicLong(0);
108 private final AtomicLong totalItemsWithErrors =
new AtomicLong(0);
109 private final AtomicLong totalWritetime =
new AtomicLong(0);
110 private final AtomicLong totalParsetime =
new AtomicLong(0);
117 PhotoRecCarverFileIngestModule(PhotoRecCarverIngestJobSettings settings) {
118 keepCorruptedFiles = settings.isKeepCorruptedFiles();
121 private static synchronized IngestJobTotals getTotalsForIngestJobs(
long ingestJobId) {
122 IngestJobTotals totals = totalsForIngestJobs.get(ingestJobId);
123 if (totals == null) {
124 totals =
new PhotoRecCarverFileIngestModule.IngestJobTotals();
125 totalsForIngestJobs.put(ingestJobId, totals);
130 private static synchronized void initTotalsForIngestJob(
long ingestJobId) {
131 IngestJobTotals totals =
new PhotoRecCarverFileIngestModule.IngestJobTotals();
132 totalsForIngestJobs.put(ingestJobId, totals);
139 public void startUp(IngestJobContext context)
throws IngestModule.IngestModuleException {
140 this.context = context;
142 this.jobId = this.context.getJobId();
148 if (!this.context.processingUnallocatedSpace()) {
149 throw new IngestModule.IngestModuleException(Bundle.unallocatedSpaceProcessingSettingsError_message());
152 this.rootOutputDirPath = createModuleOutputDirectoryForCase();
155 executableFile = locateExecutable();
157 if (PhotoRecCarverFileIngestModule.refCounter.incrementAndGet(
this.jobId) == 1) {
160 DateFormat dateFormat =
new SimpleDateFormat(
"MM-dd-yyyy-HH-mm-ss-SSSS");
161 Date date =
new Date();
162 String folder = this.context.getDataSource().getId() +
"_" + dateFormat.format(date);
163 Path outputDirPath = Paths.get(this.rootOutputDirPath.toAbsolutePath().toString(), folder);
164 Files.createDirectories(outputDirPath);
167 Path tempDirPath = Paths.get(outputDirPath.toString(), PhotoRecCarverFileIngestModule.TEMP_DIR_NAME);
168 Files.createDirectory(tempDirPath);
171 PhotoRecCarverFileIngestModule.pathsByJob.put(this.jobId,
new WorkingPaths(outputDirPath, tempDirPath));
174 initTotalsForIngestJob(jobId);
175 }
catch (SecurityException | IOException | UnsupportedOperationException ex) {
176 throw new IngestModule.IngestModuleException(Bundle.cannotCreateOutputDir_message(ex.getLocalizedMessage()), ex);
185 public IngestModule.ProcessResult process(AbstractFile file) {
187 if (file.getType() != TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS) {
188 return IngestModule.ProcessResult.OK;
192 IngestJobTotals totals = getTotalsForIngestJobs(jobId);
194 Path tempFilePath = null;
197 if (null == this.executableFile) {
198 logger.log(Level.SEVERE,
"PhotoRec carver called after failed start up");
199 return IngestModule.ProcessResult.ERROR;
205 long freeDiskSpace = IngestServices.getInstance().getFreeDiskSpace();
206 if ((freeDiskSpace != IngestMonitor.DISK_FREE_SPACE_UNKNOWN) && ((file.getSize() * 1.2) > freeDiskSpace)) {
207 logger.log(Level.SEVERE,
"PhotoRec error processing {0} with {1} Not enough space on primary disk to save unallocated space.",
208 new Object[]{file.getName(), PhotoRecCarverIngestModuleFactory.getModuleName()});
209 MessageNotifyUtil.Notify.error(NbBundle.getMessage(
this.getClass(),
"PhotoRecIngestModule.UnableToCarve", file.getName()),
210 NbBundle.getMessage(
this.getClass(),
"PhotoRecIngestModule.NotEnoughDiskSpace"));
211 return IngestModule.ProcessResult.ERROR;
213 if (this.context.fileIngestIsCancelled() ==
true) {
215 logger.log(Level.INFO,
"PhotoRec cancelled by user");
216 MessageNotifyUtil.Notify.info(PhotoRecCarverIngestModuleFactory.getModuleName(), NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
"PhotoRecIngestModule.cancelledByUser"));
217 return IngestModule.ProcessResult.OK;
221 long writestart = System.currentTimeMillis();
222 WorkingPaths paths = PhotoRecCarverFileIngestModule.pathsByJob.get(this.jobId);
223 tempFilePath = Paths.get(paths.getTempDirPath().toString(), file.getName());
224 ContentUtils.writeToFile(file, tempFilePath.toFile(), context::fileIngestIsCancelled);
226 if (this.context.fileIngestIsCancelled() ==
true) {
228 logger.log(Level.INFO,
"PhotoRec cancelled by user");
229 MessageNotifyUtil.Notify.info(PhotoRecCarverIngestModuleFactory.getModuleName(), NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
"PhotoRecIngestModule.cancelledByUser"));
230 return IngestModule.ProcessResult.OK;
234 Path outputDirPath = Paths.get(paths.getOutputDirPath().toString(), file.getName());
235 Files.createDirectory(outputDirPath);
236 File log =
new File(Paths.get(outputDirPath.toString(), LOG_FILE).toString());
239 ProcessBuilder processAndSettings =
new ProcessBuilder(
240 executableFile.toString(),
242 outputDirPath.toAbsolutePath().toString() + File.separator + PHOTOREC_RESULTS_BASE,
244 tempFilePath.toFile().toString());
245 if (keepCorruptedFiles) {
246 processAndSettings.command().add(
"options,keep_corrupted_file,search");
248 processAndSettings.command().add(
"search");
252 processAndSettings.environment().put(
"__COMPAT_LAYER",
"RunAsInvoker");
253 processAndSettings.redirectErrorStream(
true);
254 processAndSettings.redirectOutput(Redirect.appendTo(log));
256 FileIngestModuleProcessTerminator terminator =
new FileIngestModuleProcessTerminator(this.context,
true);
257 int exitValue = ExecUtil.execute(processAndSettings, terminator);
259 if (this.context.fileIngestIsCancelled() ==
true) {
261 cleanup(outputDirPath, tempFilePath);
262 logger.log(Level.INFO,
"PhotoRec cancelled by user");
263 MessageNotifyUtil.Notify.info(PhotoRecCarverIngestModuleFactory.getModuleName(), NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
"PhotoRecIngestModule.cancelledByUser"));
264 return IngestModule.ProcessResult.OK;
265 }
else if (terminator.getTerminationCode() == ProcTerminationCode.TIME_OUT) {
266 cleanup(outputDirPath, tempFilePath);
267 String msg = NbBundle.getMessage(this.getClass(),
"PhotoRecIngestModule.processTerminated") + file.getName();
268 MessageNotifyUtil.Notify.error(NbBundle.getMessage(
this.getClass(),
"PhotoRecIngestModule.moduleError"), msg);
269 logger.log(Level.SEVERE, msg);
270 return IngestModule.ProcessResult.ERROR;
271 }
else if (0 != exitValue) {
273 cleanup(outputDirPath, tempFilePath);
274 totals.totalItemsWithErrors.incrementAndGet();
275 logger.log(Level.SEVERE,
"PhotoRec carver returned error exit value = {0} when scanning {1}",
276 new Object[]{exitValue, file.getName()});
277 MessageNotifyUtil.Notify.error(PhotoRecCarverIngestModuleFactory.getModuleName(), NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
"PhotoRecIngestModule.error.exitValue",
278 new Object[]{exitValue, file.getName()}));
279 return IngestModule.ProcessResult.ERROR;
283 java.io.File oldAuditFile =
new java.io.File(Paths.get(outputDirPath.toString(), PHOTOREC_RESULTS_EXTENDED, PHOTOREC_REPORT).toString());
284 java.io.File newAuditFile =
new java.io.File(Paths.get(outputDirPath.toString(), PHOTOREC_REPORT).toString());
285 oldAuditFile.renameTo(newAuditFile);
287 if (this.context.fileIngestIsCancelled() ==
true) {
289 logger.log(Level.INFO,
"PhotoRec cancelled by user");
290 MessageNotifyUtil.Notify.info(PhotoRecCarverIngestModuleFactory.getModuleName(), NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
"PhotoRecIngestModule.cancelledByUser"));
291 return IngestModule.ProcessResult.OK;
293 Path pathToRemove = Paths.get(outputDirPath.toAbsolutePath().toString());
294 try (DirectoryStream<Path> stream = Files.newDirectoryStream(pathToRemove)) {
295 for (Path entry : stream) {
296 if (Files.isDirectory(entry)) {
297 FileUtil.deleteDir(
new File(entry.toString()));
301 long writedelta = (System.currentTimeMillis() - writestart);
302 totals.totalWritetime.addAndGet(writedelta);
305 long calcstart = System.currentTimeMillis();
306 PhotoRecCarverOutputParser parser =
new PhotoRecCarverOutputParser(outputDirPath);
307 if (this.context.fileIngestIsCancelled() ==
true) {
309 logger.log(Level.INFO,
"PhotoRec cancelled by user");
310 MessageNotifyUtil.Notify.info(PhotoRecCarverIngestModuleFactory.getModuleName(), NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
"PhotoRecIngestModule.cancelledByUser"));
311 return IngestModule.ProcessResult.OK;
313 List<LayoutFile> carvedItems = parser.parse(newAuditFile, file, context);
314 long calcdelta = (System.currentTimeMillis() - calcstart);
315 totals.totalParsetime.addAndGet(calcdelta);
316 if (carvedItems != null && !carvedItems.isEmpty()) {
317 totals.totalItemsRecovered.addAndGet(carvedItems.size());
318 context.addFilesToJob(
new ArrayList<>(carvedItems));
321 }
catch (ReadContentInputStreamException ex) {
322 totals.totalItemsWithErrors.incrementAndGet();
323 logger.log(Level.WARNING, String.format(
"Error reading file '%s' (id=%d) with the PhotoRec carver.", file.getName(), file.getId()), ex);
324 MessageNotifyUtil.Notify.error(PhotoRecCarverIngestModuleFactory.getModuleName(), NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
"PhotoRecIngestModule.error.msg", file.getName()));
325 return IngestModule.ProcessResult.ERROR;
326 }
catch (IOException ex) {
327 totals.totalItemsWithErrors.incrementAndGet();
328 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);
329 MessageNotifyUtil.Notify.error(PhotoRecCarverIngestModuleFactory.getModuleName(), NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
"PhotoRecIngestModule.error.msg", file.getName()));
330 return IngestModule.ProcessResult.ERROR;
332 if (null != tempFilePath && Files.exists(tempFilePath)) {
334 tempFilePath.toFile().delete();
337 return IngestModule.ProcessResult.OK;
341 private void cleanup(Path outputDirPath, Path tempFilePath) {
343 FileUtil.deleteDir(
new File(outputDirPath.toString()));
344 if (null != tempFilePath && Files.exists(tempFilePath)) {
345 tempFilePath.toFile().delete();
349 private static synchronized void postSummary(
long jobId) {
350 IngestJobTotals jobTotals = totalsForIngestJobs.remove(jobId);
352 StringBuilder detailsSb =
new StringBuilder();
354 detailsSb.append(
"<table border='0' cellpadding='4' width='280'>");
356 detailsSb.append(
"<tr><td>")
357 .append(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
"PhotoRecIngestModule.complete.numberOfCarved"))
359 detailsSb.append(
"<td>").append(jobTotals.totalItemsRecovered.get()).append(
"</td></tr>");
361 detailsSb.append(
"<tr><td>")
362 .append(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
"PhotoRecIngestModule.complete.numberOfErrors"))
364 detailsSb.append(
"<td>").append(jobTotals.totalItemsWithErrors.get()).append(
"</td></tr>");
366 detailsSb.append(
"<tr><td>")
367 .append(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
"PhotoRecIngestModule.complete.totalWritetime"))
368 .append(
"</td><td>").append(jobTotals.totalWritetime.get()).append(
"</td></tr>\n");
369 detailsSb.append(
"<tr><td>")
370 .append(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
"PhotoRecIngestModule.complete.totalParsetime"))
371 .append(
"</td><td>").append(jobTotals.totalParsetime.get()).append(
"</td></tr>\n");
372 detailsSb.append(
"</table>");
374 IngestServices.getInstance().postMessage(IngestMessage.createMessage(
375 IngestMessage.MessageType.INFO,
376 PhotoRecCarverIngestModuleFactory.getModuleName(),
377 NbBundle.getMessage(PhotoRecCarverFileIngestModule.class,
378 "PhotoRecIngestModule.complete.photoRecResults"),
379 detailsSb.toString()));
387 public void shutDown() {
388 if (this.context != null && refCounter.decrementAndGet(
this.jobId) == 0) {
392 WorkingPaths paths = PhotoRecCarverFileIngestModule.pathsByJob.remove(this.jobId);
393 FileUtil.deleteDir(
new File(paths.getTempDirPath().toString()));
395 }
catch (SecurityException ex) {
396 logger.log(Level.SEVERE,
"Error shutting down PhotoRec carver module", ex);
407 this.outputDirPath = outputDirPath;
408 this.tempDirPath = tempDirPath;
411 Path getOutputDirPath() {
412 return this.outputDirPath;
415 Path getTempDirPath() {
416 return this.tempDirPath;
428 synchronized Path createModuleOutputDirectoryForCase() throws IngestModule.IngestModuleException {
431 path = Paths.get(Case.getCurrentCaseThrows().getModuleDirectory(), PhotoRecCarverIngestModuleFactory.getModuleName());
432 }
catch (NoCurrentCaseException ex) {
433 throw new IngestModule.IngestModuleException(Bundle.cannotCreateOutputDir_message(ex.getLocalizedMessage()), ex);
436 Files.createDirectory(path);
437 if (UNCPathUtilities.isUNC(path)) {
441 throw new IngestModule.IngestModuleException(Bundle.PhotoRecIngestModule_nonHostnameUNCPathUsed());
443 if (
false == FileUtil.hasReadWriteAccess(path)) {
444 throw new IngestModule.IngestModuleException(
445 Bundle.PhotoRecIngestModule_PermissionsNotSufficient() + SEP + path.toString() + SEP
446 + Bundle.PhotoRecIngestModule_PermissionsNotSufficientSeeReference()
450 }
catch (FileAlreadyExistsException ex) {
452 }
catch (IOException | SecurityException | UnsupportedOperationException ex) {
453 throw new IngestModule.IngestModuleException(Bundle.cannotCreateOutputDir_message(ex.getLocalizedMessage()), ex);
468 public static File locateExecutable() throws IngestModule.IngestModuleException {
471 String photorec_linux_directory =
"/usr/bin";
472 if (PlatformUtil.isWindowsOS()) {
473 execName = Paths.get(PHOTOREC_DIRECTORY, PHOTOREC_SUBDIRECTORY, PHOTOREC_EXECUTABLE);
474 exeFile = InstalledFileLocator.getDefault().locate(execName.toString(), PhotoRecCarverFileIngestModule.class.getPackage().getName(),
false);
476 File usrBin =
new File(
"/usr/bin/photorec");
477 File usrLocalBin =
new File(
"/usr/local/bin/photorec");
478 if (usrBin.canExecute() && usrBin.exists() && !usrBin.isDirectory()) {
479 photorec_linux_directory =
"/usr/bin";
480 }
else if(usrLocalBin.canExecute() && usrLocalBin.exists() && !usrLocalBin.isDirectory()){
481 photorec_linux_directory =
"/usr/local/bin";
483 throw new IngestModule.IngestModuleException(
"Photorec not found");
485 execName = Paths.get(photorec_linux_directory, PHOTOREC_LINUX_EXECUTABLE);
486 exeFile =
new File(execName.toString());
489 if (null == exeFile) {
490 throw new IngestModule.IngestModuleException(Bundle.missingExecutable_message());
494 if (!exeFile.canExecute()) {
495 throw new IngestModule.IngestModuleException(Bundle.cannotRunExecutable_message());
void fireModuleContentEvent(ModuleContentEvent moduleContentEvent)
synchronized static Logger getLogger(String name)
synchronized Path ipToHostName(Path inputPath)
static synchronized IngestServices getInstance()