/*
* Autopsy Forensic Browser
*
* Copyright 2015 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> 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.experimental.autoingest;
import java.awt.Dimension;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.logging.Level;
import static javax.security.auth.callback.ConfirmationCallback.OK_CANCEL_OPTION;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import org.apache.commons.io.FileUtils;
import org.openide.util.NbBundle;
import org.openide.windows.WindowManager;
import org.sleuthkit.autopsy.casemodule.SingleUserCaseConverter;
import org.sleuthkit.autopsy.casemodule.SingleUserCaseConverter.ImportCaseData;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.NetworkUtils;
public class SingleUserCaseImporter implements Runnable {
private static final String AIM_LOG_FILE_NAME = "auto_ingest_log.txt"; //NON-NLS
static final String CASE_IMPORT_LOG_FILE = "case_import_log.txt"; //NON-NLS
private static final String DOTAUT = ".aut"; //NON-NLS
private static final String SEP = System.getProperty("line.separator");
private static final String logDateFormat = "yyyy/MM/dd HH:mm:ss"; //NON-NLS
private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(logDateFormat);
private final Object threadWaitNotifyLock = new Object();
private final ImportDoneCallback notifyOnComplete;
private final Path baseImageInput;
private final Path baseCaseInput;
private final Path baseImageOutput;
private final Path baseCaseOutput;
private final boolean copyImages;
private final boolean deleteCase;
private String oldCaseName = null;
private String newCaseName = null;
private int userAnswer = 0;
private PrintWriter writer;
public SingleUserCaseImporter(String baseImageInput, String baseCaseInput, String baseImageOutput, String baseCaseOutput, boolean copyImages, boolean deleteCase, ImportDoneCallback callback) {
this.baseImageInput = Paths.get(baseImageInput);
this.baseCaseInput = Paths.get(baseCaseInput);
this.baseImageOutput = Paths.get(baseImageOutput);
this.baseCaseOutput = Paths.get(baseCaseOutput);
this.copyImages = copyImages;
this.deleteCase = deleteCase;
this.notifyOnComplete = callback;
}
/**
* This causes iteration over all .aut files in the baseCaseInput path,
* calling SingleUserCaseConverter.importCase() for each one.
*/
public void importCases() throws Exception {
openLog(baseCaseOutput.toFile());
log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.StartingBatch")
+ baseCaseInput.toString() + " "
+ NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.to")
+ " " + baseCaseOutput.toString()); //NON-NLS
// iterate for .aut files
FindDotAutFolders dotAutFolders = new FindDotAutFolders();
try {
Path walked = Files.walkFileTree(baseCaseInput, dotAutFolders);
} catch (IOException ex) {
log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.ErrorFindingAutFiles") + " " + ex.getMessage()); //NON-NLS
}
ArrayList<ImportCaseData> ableToProcess = new ArrayList<>();
ArrayList<ImportCaseData> unableToProcess = new ArrayList<>();
SingleUserCaseConverter scc = new SingleUserCaseConverter();
// validate we can convert the .aut file, one by one
for (FoundAutFile f : dotAutFolders.getCandidateList()) {
this.oldCaseName = f.getPath().getFileName().toString();
// Test image output folder for uniqueness, find a unique folder for it if we can
File specificOutputFolder = baseImageOutput.resolve(oldCaseName).toFile();
String newImageName = oldCaseName;
if (specificOutputFolder.exists()) {
// Not unique. add numbers before timestamp to specific image output name
String timeStamp = TimeStampUtils.getTimeStampOnly(oldCaseName);
newImageName = TimeStampUtils.removeTimeStamp(oldCaseName);
int number = 1;
String temp = ""; //NON-NLS
while (specificOutputFolder.exists()) {
if (number == Integer.MAX_VALUE) {
// It never became unique, so give up.
throw new Exception(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.NonUniqueOutputFolder") + newImageName); //NON-NLS
}
temp = newImageName + "_" + Integer.toString(number) + timeStamp; //NON-NLS
specificOutputFolder = baseImageOutput.resolve(temp).toFile();
++number;
}
newImageName = temp;
}
Path imageOutput = baseImageOutput.resolve(newImageName);
imageOutput.toFile().mkdirs(); // Create image output folder
// Test case output folder for uniqueness, find a unique folder for it if we can
specificOutputFolder = baseCaseOutput.resolve(oldCaseName).toFile();
newCaseName = oldCaseName;
if (specificOutputFolder.exists()) {
// not unique. add numbers before timestamp to specific case output name
String timeStamp = TimeStampUtils.getTimeStampOnly(oldCaseName); //NON-NLS
newCaseName = TimeStampUtils.removeTimeStamp(oldCaseName);
int number = 1;
String temp = ""; //NON-NLS
while (specificOutputFolder.exists()) {
if (number == Integer.MAX_VALUE) {
// It never became unique, so give up.
throw new Exception(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.NonUniqueOutputFolder") + newCaseName); //NON-NLS
}
temp = newCaseName + "_" + Integer.toString(number) + timeStamp; //NON-NLS
specificOutputFolder = baseCaseOutput.resolve(temp).toFile();
++number;
}
newCaseName = temp;
}
Path caseOutput = baseCaseOutput.resolve(newCaseName);
caseOutput.toFile().mkdirs(); // Create case output folder
/**
* Test if the input path has a corresponding image input folder and
* no repeated case names in the path. If both of these conditions
* are true, we can process this case, otherwise not.
*/
// Check that there is an image folder if they are trying to copy it
boolean canProcess = true;
Path imageInput = null;
String relativeCaseName = TimeStampUtils.removeTimeStamp(baseCaseInput.relativize(f.getPath()).toString());
Path testImageInputsFromOldCase = Paths.get(baseImageInput.toString(), relativeCaseName);
if (copyImages) {
if (!testImageInputsFromOldCase.toFile().isDirectory()) {
// Mark that we are unable to process this item
canProcess = false;
} else {
imageInput = testImageInputsFromOldCase;
}
if (imageInput == null) {
throw new Exception(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.SourceImageMissing") + " " + f.getPath()); //NON-NLS
}
// If case name is in the image path, it causes bad things to happen with the parsing. Test for this.
for (int x = 0; x < imageInput.getNameCount(); ++x) {
if (oldCaseName.toLowerCase().equals(imageInput.getName(x).toString().toLowerCase())) {
// Mark that we are unable to process this item
canProcess = false;
}
}
} else {
imageInput = testImageInputsFromOldCase;
}
// Create an Import Case Data object for this case
SingleUserCaseConverter.ImportCaseData icd = scc.new ImportCaseData(
imageInput,
f.getPath(),
imageOutput,
caseOutput,
oldCaseName,
newCaseName,
f.getAutFile().toString(),
f.getFolderName().toString(),
copyImages,
deleteCase);
if (canProcess) {
ableToProcess.add(icd);
} else {
unableToProcess.add(icd);
}
}
// Create text to be populated in the confirmation dialog
StringBuilder casesThatWillBeProcessed = new StringBuilder();
StringBuilder casesThatWillNotBeProcessed = new StringBuilder();
casesThatWillBeProcessed.append(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.WillImport")).append(SEP); // NON-NLS
if (ableToProcess.isEmpty()) {
casesThatWillBeProcessed.append(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.None")).append(SEP); // NON-NLS
} else {
for (ImportCaseData i : ableToProcess) {
casesThatWillBeProcessed.append(i.getCaseInputFolder().toString()).append(SEP);
}
}
if (!unableToProcess.isEmpty()) {
casesThatWillNotBeProcessed.append(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.WillNotImport")).append(SEP); // NON-NLS
for (ImportCaseData i : unableToProcess) {
casesThatWillNotBeProcessed.append(i.getCaseInputFolder().toString()).append(SEP);
}
}
JTextArea jta = new JTextArea(casesThatWillBeProcessed.toString() + SEP + casesThatWillNotBeProcessed.toString());
jta.setEditable(false);
JScrollPane jsp = new JScrollPane(jta) {
private static final long serialVersionUID = 1L;
@Override
public Dimension getPreferredSize() {
return new Dimension(700, 480);
}
};
// Show confirmation dialog
SwingUtilities.invokeLater(() -> {
userAnswer = JOptionPane.showConfirmDialog(WindowManager.getDefault().getMainWindow(),
jsp,
NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.ContinueWithImport"), // NON-NLS
OK_CANCEL_OPTION);
synchronized (threadWaitNotifyLock) {
threadWaitNotifyLock.notify();
}
});
// Wait while the user handles the confirmation dialog
synchronized (threadWaitNotifyLock) {
try {
threadWaitNotifyLock.wait();
} catch (InterruptedException ex) {
Logger.getLogger(SingleUserCaseImporter.class.getName()).log(Level.SEVERE, "Threading Issue", ex); //NON-NLS
throw new Exception(ex);
}
}
// If the user wants to proceed, do so.
if (userAnswer == JOptionPane.OK_OPTION) {
boolean result = true; // if anything went wrong, result becomes false.
// Feed .aut files in one by one for processing
for (ImportCaseData i : ableToProcess) {
try {
log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.StartedProcessing")
+ i.getCaseInputFolder()
+ " " + NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.to") + " "
+ i.getCaseOutputFolder()); //NON-NLS
SingleUserCaseConverter.importCase(i);
handleAutoIngestLog(i);
log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.FinishedProcessing")
+ i.getCaseInputFolder()
+ " " + NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.to") + " "
+ i.getCaseOutputFolder()); //NON-NLS
} catch (Exception ex) {
log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.FailedToComplete")
+ i.getCaseInputFolder()
+ " " + NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.to") + " "
+ i.getCaseOutputFolder() + " " + ex.getMessage()); //NON-NLS
result = false;
}
}
log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.CompletedBatch")
+ baseCaseInput.toString()
+ " " + NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.to") + " "
+ baseCaseOutput.toString()); //NON-NLS
closeLog();
if (notifyOnComplete != null) {
notifyOnComplete.importDoneCallback(result, ""); // NON-NLS
}
} else {
// The user clicked cancel. Abort.
log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.AbortingBatch")
+ baseCaseInput.toString()
+ " " + NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.to") + " "
+ baseCaseOutput.toString()); //NON-NLS
closeLog();
if (notifyOnComplete != null) {
notifyOnComplete.importDoneCallback(false, NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.Cancelled")); // NON-NLS
}
}
}
@Override
public void run() {
try {
importCases();
} catch (Exception ex) {
log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.FailedToComplete")
+ baseCaseInput.toString()
+ " " + NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.to") + " "
+ baseCaseOutput.toString()
+ " " + ex.getMessage()); //NON-NLS
closeLog();
if (notifyOnComplete != null) {
notifyOnComplete.importDoneCallback(false, ex.getMessage()); // NON-NLS
}
}
}
/**
* Move the Auto Ingest log if we can
*
* @param icd the Import Case Data structure detailing where the files are
*/
void handleAutoIngestLog(ImportCaseData icd) {
try {
Path source = icd.getCaseInputFolder().resolve(AIM_LOG_FILE_NAME);
Path destination = icd.getCaseOutputFolder().resolve(AIM_LOG_FILE_NAME);
if (source.toFile().exists()) {
FileUtils.copyFile(source.toFile(), destination.toFile());
}
try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(destination.toString(), true)))) {
out.println(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.ImportedAsMultiUser") + new Date()); //NON-NLS
} catch (IOException e) {
// If unable to log it, no problem, move on
}
File oldIngestLog = Paths.get(icd.getCaseOutputFolder().toString(), NetworkUtils.getLocalHostName(), AIM_LOG_FILE_NAME).toFile();
if (oldIngestLog.exists()) {
oldIngestLog.delete();
}
} catch (Exception ex) {
// If unable to copy Auto Ingest log, no problem, move on
}
}
/**
* Open the case import log in the base output folder.
*
* @param location holds the path to the log file
*/
private void openLog(File location) {
location.mkdirs();
File logFile = Paths.get(location.toString(), CASE_IMPORT_LOG_FILE).toFile();
try {
writer = new PrintWriter(new BufferedWriter(new FileWriter(logFile, logFile.exists())), true);
} catch (IOException ex) {
writer = null;
Logger.getLogger(SingleUserCaseImporter.class.getName()).log(Level.WARNING, "Error opening log file " + logFile.toString(), ex); //NON-NLS
}
}
/**
* Log a message to the case import log in the base output folder.
*
* @param message the message to log.
*/
private void log(String message) {
if (writer != null) {
writer.println(String.format("%s %s", simpleDateFormat.format((Date.from(Instant.now()).getTime())), message)); //NON-NLS
}
}
/**
* Close the case import log
*/
private void closeLog() {
if (writer != null) {
writer.close();
}
}
/**
* Extend SimpleFileVisitor to find all the cases to process based upon
* presence of .aut files.
*/
private class FindDotAutFolders extends SimpleFileVisitor<Path> {
private final ArrayList<FoundAutFile> candidateList;
public FindDotAutFolders() {
this.candidateList = new ArrayList<>();
}
/**
* Handle comparing .aut file and containing folder names without
* timestamps on either one. It strips them off if they exist.
*
* @param directory the directory we are currently visiting.
* @param attrs file attributes.
*
* @return CONTINUE if we want to carry on, SKIP_SUBTREE if we've found
* a .aut file, precluding searching any deeper into this
* folder.
*
* @throws IOException
*/
@Override
public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attrs) throws IOException {
// Find all files that end in .aut
File[] dotAutFiles = directory.toFile().listFiles((File dir, String name) -> name.toLowerCase().endsWith(DOTAUT));
for (File specificFile : dotAutFiles) {
// If the case name ends in a timestamp, strip it off
String sanitizedCaseName = specificFile.getName();
sanitizedCaseName = TimeStampUtils.removeTimeStamp(sanitizedCaseName);
// If the folder ends in a timestamp, strip it off
String sanitizedFolderName = TimeStampUtils.removeTimeStamp(directory.getFileName().toString());
// If file and folder match, found leaf node case
if (sanitizedCaseName.toLowerCase().startsWith(sanitizedFolderName.toLowerCase())) {
candidateList.add(new FoundAutFile(directory, Paths.get(sanitizedCaseName), Paths.get(sanitizedFolderName)));
return FileVisitResult.SKIP_SUBTREE;
}
}
// If no matching .aut files, continue to traverse subfolders
return FileVisitResult.CONTINUE;
}
/**
* Returns the list of folders we've found that need to be looked at for
* possible import from single-user to multi-user cases.
*
* @return the candidateList
*/
public ArrayList<FoundAutFile> getCandidateList() {
return candidateList;
}
}
/**
* This class holds information about .aut files that have been found by the
* FileWalker.
*/
public class FoundAutFile {
private final Path path;
private final Path autFile;
private final Path folderName;
public FoundAutFile(Path path, Path autFile, Path folderName) {
this.path = path;
this.autFile = autFile;
this.folderName = folderName;
}
Path getPath() {
return this.path;
}
Path getAutFile() {
return this.autFile;
}
Path getFolderName() {
return this.folderName;
}
}
}