mirror of
https://github.com/overcuriousity/autopsy-flatpak.git
synced 2025-07-17 18:17:43 +00:00
initial work towards custom file content reader
This commit is contained in:
parent
39490cd95c
commit
9c4766d97e
@ -103,6 +103,7 @@ import org.sleuthkit.autopsy.casemodule.events.TagNamesEvent.TagNamesDeletedEven
|
|||||||
import org.sleuthkit.autopsy.casemodule.events.TagNamesEvent.TagNamesUpdatedEvent;
|
import org.sleuthkit.autopsy.casemodule.events.TagNamesEvent.TagNamesUpdatedEvent;
|
||||||
import org.sleuthkit.autopsy.casemodule.events.TagSetsEvent.TagSetsAddedEvent;
|
import org.sleuthkit.autopsy.casemodule.events.TagSetsEvent.TagSetsAddedEvent;
|
||||||
import org.sleuthkit.autopsy.casemodule.events.TagSetsEvent.TagSetsDeletedEvent;
|
import org.sleuthkit.autopsy.casemodule.events.TagSetsEvent.TagSetsDeletedEvent;
|
||||||
|
import org.sleuthkit.autopsy.casemodule.filecontent.CustomFileContentProvider;
|
||||||
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData.CaseNodeDataException;
|
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData.CaseNodeDataException;
|
||||||
import org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils;
|
import org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils;
|
||||||
import org.sleuthkit.autopsy.casemodule.services.Services;
|
import org.sleuthkit.autopsy.casemodule.services.Services;
|
||||||
@ -2743,9 +2744,11 @@ public class Case {
|
|||||||
try {
|
try {
|
||||||
String databaseName = metadata.getCaseDatabaseName();
|
String databaseName = metadata.getCaseDatabaseName();
|
||||||
if (CaseType.SINGLE_USER_CASE == metadata.getCaseType()) {
|
if (CaseType.SINGLE_USER_CASE == metadata.getCaseType()) {
|
||||||
caseDb = SleuthkitCase.openCase(Paths.get(metadata.getCaseDirectory(), databaseName).toString());
|
caseDb = SleuthkitCase.openCase(Paths.get(metadata.getCaseDirectory(), databaseName).toString(),
|
||||||
|
CustomFileContentProvider.getProvider(metadata.getFileContentPath()));
|
||||||
} else if (UserPreferences.getIsMultiUserModeEnabled()) {
|
} else if (UserPreferences.getIsMultiUserModeEnabled()) {
|
||||||
caseDb = SleuthkitCase.openCase(databaseName, UserPreferences.getDatabaseConnectionInfo(), metadata.getCaseDirectory());
|
caseDb = SleuthkitCase.openCase(databaseName, UserPreferences.getDatabaseConnectionInfo(), metadata.getCaseDirectory(),
|
||||||
|
CustomFileContentProvider.getProvider(metadata.getFileContentPath()));
|
||||||
} else {
|
} else {
|
||||||
throw new CaseActionException(Bundle.Case_open_exception_multiUserCaseNotEnabled());
|
throw new CaseActionException(Bundle.Case_open_exception_multiUserCaseNotEnabled());
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,7 @@ import javax.xml.transform.TransformerException;
|
|||||||
import javax.xml.transform.TransformerFactory;
|
import javax.xml.transform.TransformerFactory;
|
||||||
import javax.xml.transform.dom.DOMSource;
|
import javax.xml.transform.dom.DOMSource;
|
||||||
import javax.xml.transform.stream.StreamResult;
|
import javax.xml.transform.stream.StreamResult;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.sleuthkit.autopsy.coreutils.Version;
|
import org.sleuthkit.autopsy.coreutils.Version;
|
||||||
import org.sleuthkit.autopsy.coreutils.XMLUtil;
|
import org.sleuthkit.autopsy.coreutils.XMLUtil;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
@ -102,14 +103,21 @@ public final class CaseMetadata {
|
|||||||
*/
|
*/
|
||||||
private static final String SCHEMA_VERSION_FIVE = "5.0";
|
private static final String SCHEMA_VERSION_FIVE = "5.0";
|
||||||
private final static String ORIGINAL_CASE_ELEMENT_NAME = "OriginalCase"; //NON-NLS
|
private final static String ORIGINAL_CASE_ELEMENT_NAME = "OriginalCase"; //NON-NLS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields from schema version 6
|
||||||
|
*/
|
||||||
|
private static final String SCHEMA_VERSION_SIX = "6.0";
|
||||||
|
// specifies a path to where file content resides
|
||||||
|
private static final String FILE_CONTENT_PATH = "FileContentPath";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Unread fields, regenerated on save.
|
* Unread fields, regenerated on save.
|
||||||
*/
|
*/
|
||||||
private final static String MODIFIED_DATE_ELEMENT_NAME = "ModifiedDate"; //NON-NLS
|
private final static String MODIFIED_DATE_ELEMENT_NAME = "ModifiedDate"; //NON-NLS
|
||||||
private final static String AUTOPSY_SAVED_BY_ELEMENT_NAME = "SavedByAutopsyVersion"; //NON-NLS
|
private final static String AUTOPSY_SAVED_BY_ELEMENT_NAME = "SavedByAutopsyVersion"; //NON-NLS
|
||||||
|
|
||||||
private final static String CURRENT_SCHEMA_VERSION = SCHEMA_VERSION_FIVE;
|
private final static String CURRENT_SCHEMA_VERSION = SCHEMA_VERSION_SIX;
|
||||||
|
|
||||||
private final Path metadataFilePath;
|
private final Path metadataFilePath;
|
||||||
private Case.CaseType caseType;
|
private Case.CaseType caseType;
|
||||||
@ -120,6 +128,7 @@ public final class CaseMetadata {
|
|||||||
private String textIndexName; // Legacy
|
private String textIndexName; // Legacy
|
||||||
private String createdDate;
|
private String createdDate;
|
||||||
private String createdByVersion;
|
private String createdByVersion;
|
||||||
|
private String fileContentPath;
|
||||||
private CaseMetadata originalMetadata = null; // For portable cases
|
private CaseMetadata originalMetadata = null; // For portable cases
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -257,7 +266,15 @@ public final class CaseMetadata {
|
|||||||
public CaseDetails getCaseDetails() {
|
public CaseDetails getCaseDetails() {
|
||||||
return caseDetails;
|
return caseDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The template path for where file content bytes resides. This is only used if non-null.
|
||||||
|
*/
|
||||||
|
public String getFileContentPath() {
|
||||||
|
return fileContentPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the case display name.
|
* Gets the case display name.
|
||||||
*
|
*
|
||||||
@ -334,6 +351,24 @@ public final class CaseMetadata {
|
|||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the path template for file content bytes. This is only used if non-null.
|
||||||
|
*
|
||||||
|
* @param fileContentPath The template for the file content path.
|
||||||
|
*
|
||||||
|
* @throws CaseMetadataException If the operation fails.
|
||||||
|
*/
|
||||||
|
void setFileContentPath(String fileContentPath) throws CaseMetadataException {
|
||||||
|
String oldFileContentPath = this.fileContentPath;
|
||||||
|
this.fileContentPath = fileContentPath;
|
||||||
|
try {
|
||||||
|
writeToFile();
|
||||||
|
} catch (CaseMetadataException ex) {
|
||||||
|
this.fileContentPath = oldFileContentPath;
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the text index name. This is a legacy field and will be empty for
|
* Gets the text index name. This is a legacy field and will be empty for
|
||||||
@ -497,6 +532,7 @@ public final class CaseMetadata {
|
|||||||
createChildElement(doc, caseElement, CASE_DB_ABSOLUTE_PATH_ELEMENT_NAME, metadataToWrite.caseDatabasePath);
|
createChildElement(doc, caseElement, CASE_DB_ABSOLUTE_PATH_ELEMENT_NAME, metadataToWrite.caseDatabasePath);
|
||||||
createChildElement(doc, caseElement, CASE_DB_NAME_RELATIVE_ELEMENT_NAME, metadataToWrite.caseDatabaseName);
|
createChildElement(doc, caseElement, CASE_DB_NAME_RELATIVE_ELEMENT_NAME, metadataToWrite.caseDatabaseName);
|
||||||
createChildElement(doc, caseElement, TEXT_INDEX_ELEMENT, metadataToWrite.textIndexName);
|
createChildElement(doc, caseElement, TEXT_INDEX_ELEMENT, metadataToWrite.textIndexName);
|
||||||
|
createChildElement(doc, caseElement, FILE_CONTENT_PATH, metadataToWrite.getFileContentPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -594,6 +630,15 @@ public final class CaseMetadata {
|
|||||||
this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_ELEMENT, false);
|
this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_ELEMENT, false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.fileContentPath = null;
|
||||||
|
NodeList caseElementChildren = caseElement.getElementsByTagName(FILE_CONTENT_PATH);
|
||||||
|
if (caseElementChildren.getLength() == 1) {
|
||||||
|
String fileContentTextPath = caseElementChildren.item(0).getTextContent();
|
||||||
|
if (StringUtils.isNotBlank(fileContentTextPath)) {
|
||||||
|
this.fileContentPath = fileContentTextPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Fix up the case database name due to a bug that for a time caused
|
* Fix up the case database name due to a bug that for a time caused
|
||||||
|
@ -0,0 +1,252 @@
|
|||||||
|
/*
|
||||||
|
* Autopsy Forensic Browser
|
||||||
|
*
|
||||||
|
* Copyright 2023 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.casemodule.filecontent;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.commons.text.StringSubstitutor;
|
||||||
|
import org.apache.commons.text.lookup.StringLookup;
|
||||||
|
import org.sleuthkit.datamodel.AbstractFile;
|
||||||
|
import org.sleuthkit.datamodel.FileContentStream;
|
||||||
|
import org.sleuthkit.datamodel.FileContentStream.FileContentProvider;
|
||||||
|
import org.sleuthkit.datamodel.TskCoreException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class CustomFileContentProvider implements FileContentProvider {
|
||||||
|
|
||||||
|
private static final String MD5_KEY = "md5";
|
||||||
|
private static final String APP_DATA_DIR_KEY = "appdatadir";
|
||||||
|
private static final String USER_DIR_KEY = "userdir";
|
||||||
|
private static final String APPLICATION_DIR_KEY = "applicationdir";
|
||||||
|
|
||||||
|
private static String namedGroup(String key, String regex, boolean required) {
|
||||||
|
return "(?<" + key + ">" + regex + ")" + ((required) ? "" : "?");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String VAR_KEY = "var";
|
||||||
|
private static final String SUBSTR_KEY = "substr";
|
||||||
|
private static final String FROM_IDX_KEY = "fromidx";
|
||||||
|
private static final String COLON_KEY = "colon";
|
||||||
|
private static final String TO_IDX_KEY = "toidx";
|
||||||
|
private static final String INT_REGEX_STR = "\\-?\\d+?";
|
||||||
|
|
||||||
|
// processes variables kind of like python substrings (i.e. varname[1:3] )
|
||||||
|
private static final Pattern VAR_REGEX = Pattern.compile("^\\s*"
|
||||||
|
+ namedGroup(VAR_KEY, "[a-zA-Z0-9\\\\-_\\\\.]+?", true)
|
||||||
|
+ namedGroup(SUBSTR_KEY,
|
||||||
|
"\\s*"
|
||||||
|
+ "\\["
|
||||||
|
+ "\\s*"
|
||||||
|
+ namedGroup(FROM_IDX_KEY, INT_REGEX_STR, false)
|
||||||
|
+ "\\s*"
|
||||||
|
+ namedGroup(COLON_KEY, ":", false)
|
||||||
|
+ "\\s*"
|
||||||
|
+ namedGroup(TO_IDX_KEY, INT_REGEX_STR, false)
|
||||||
|
+ "\\s*"
|
||||||
|
+ "\\]",
|
||||||
|
false)
|
||||||
|
+ "\\s*$");
|
||||||
|
|
||||||
|
private static final String SUB_DELIM = "$";
|
||||||
|
private static final char DELIMITER = SUB_DELIM.charAt(0);
|
||||||
|
private static final String VAR_PREFIX = SUB_DELIM;
|
||||||
|
private static final String VAR_SUFFIX = SUB_DELIM;
|
||||||
|
|
||||||
|
private final String stringTemplate;
|
||||||
|
private final GlobalVars globalVars;
|
||||||
|
|
||||||
|
public static CustomFileContentProvider getProvider(String stringTemplate) throws IllegalStateException {
|
||||||
|
return StringUtils.isBlank(stringTemplate)
|
||||||
|
? null
|
||||||
|
: new CustomFileContentProvider(stringTemplate, GlobalVars.getDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomFileContentProvider(String stringTemplate, GlobalVars globalVars) {
|
||||||
|
this.globalVars = globalVars;
|
||||||
|
//this.stringSub =
|
||||||
|
this.stringTemplate = stringTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileContentStream getFileContentStream(AbstractFile af) throws TskCoreException {
|
||||||
|
File localFile = getFilePath(af);
|
||||||
|
if (localFile != null && localFile.exists() && localFile.isFile()) {
|
||||||
|
try {
|
||||||
|
RandomAccessFile fileHandle = new RandomAccessFile(localFile, "r");
|
||||||
|
return new CustomFileContentStream(fileHandle);
|
||||||
|
} catch (FileNotFoundException ex) {
|
||||||
|
throw new TskCoreException("File could not be read", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
File getFilePath(AbstractFile af) {
|
||||||
|
StringLookup lookup = (key) -> getKeyValue(key, af, this.globalVars);
|
||||||
|
StringSubstitutor stringSub = new StringSubstitutor(lookup, VAR_PREFIX, VAR_SUFFIX, DELIMITER);
|
||||||
|
String filePath = stringSub.replace(stringTemplate);
|
||||||
|
|
||||||
|
return new File(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getKeyValue(String key, AbstractFile af, GlobalVars globalVars) {
|
||||||
|
// variable regex processing
|
||||||
|
Matcher matcher = VAR_REGEX.matcher(key);
|
||||||
|
if (!matcher.find()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
String variable = matcher.group(VAR_KEY).toLowerCase();
|
||||||
|
|
||||||
|
Integer fromIdx = null;
|
||||||
|
Integer toIdx = null;
|
||||||
|
boolean hasColon = false;
|
||||||
|
|
||||||
|
String fromIdxStr = matcher.group(FROM_IDX_KEY);
|
||||||
|
String toIdxStr = matcher.group(TO_IDX_KEY);
|
||||||
|
if (StringUtils.isNotBlank(toIdxStr) || StringUtils.isNotBlank(fromIdxStr)) {
|
||||||
|
toIdx = tryParse(toIdxStr);
|
||||||
|
fromIdx = tryParse(fromIdxStr);
|
||||||
|
hasColon = StringUtils.isNotBlank(matcher.group(COLON_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
String varVal = getVarVal(variable, af, globalVars);
|
||||||
|
|
||||||
|
if (toIdx != null || fromIdx != null) {
|
||||||
|
if (fromIdx == null) {
|
||||||
|
fromIdx = 0;
|
||||||
|
} else if (fromIdx < 0) {
|
||||||
|
fromIdx = varVal.length() + fromIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toIdx == null) {
|
||||||
|
if (hasColon) {
|
||||||
|
toIdx = varVal.length();
|
||||||
|
} else {
|
||||||
|
toIdx = fromIdx + 1;
|
||||||
|
}
|
||||||
|
} else if (toIdx < 0) {
|
||||||
|
toIdx = varVal.length() + fromIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
return varVal.substring(fromIdx, toIdx);
|
||||||
|
} else {
|
||||||
|
return varVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getVarVal(String var, AbstractFile af, GlobalVars globalVars) {
|
||||||
|
switch (var) {
|
||||||
|
case MD5_KEY:
|
||||||
|
return af.getMd5Hash();
|
||||||
|
case APP_DATA_DIR_KEY:
|
||||||
|
return globalVars.getAppDataDir();
|
||||||
|
case USER_DIR_KEY:
|
||||||
|
return globalVars.getUserDir();
|
||||||
|
case APPLICATION_DIR_KEY:
|
||||||
|
return globalVars.getApplicationDir();
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Integer tryParse(String intVal) {
|
||||||
|
if (StringUtils.isNotBlank(intVal)) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(intVal);
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
// ignore and just return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class GlobalVars {
|
||||||
|
|
||||||
|
private final String appDataDir;
|
||||||
|
private final String userDir;
|
||||||
|
private final String applicationDir;
|
||||||
|
|
||||||
|
public static GlobalVars getDefault() throws IllegalStateException {
|
||||||
|
try {
|
||||||
|
return new GlobalVars(
|
||||||
|
// taken from https://stackoverflow.com/a/1198954/2375948
|
||||||
|
System.getenv("APPDATA"),
|
||||||
|
// taken from https://stackoverflow.com/a/586345/2375948
|
||||||
|
System.getProperty("user.home"),
|
||||||
|
// taken from https://stackoverflow.com/a/4033033/2375948
|
||||||
|
new File(".").getCanonicalPath()
|
||||||
|
);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new IllegalStateException("Unable to get application directory", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalVars(String appDataDir, String userDir, String applicationDir) {
|
||||||
|
this.appDataDir = appDataDir;
|
||||||
|
this.userDir = userDir;
|
||||||
|
this.applicationDir = applicationDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAppDataDir() {
|
||||||
|
return appDataDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserDir() {
|
||||||
|
return userDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getApplicationDir() {
|
||||||
|
return applicationDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class CustomFileContentStream implements FileContentStream {
|
||||||
|
|
||||||
|
private final RandomAccessFile localFileHandle;
|
||||||
|
|
||||||
|
public CustomFileContentStream(RandomAccessFile localFileHandle) {
|
||||||
|
this.localFileHandle = localFileHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buf, long offset, long len) throws TskCoreException {
|
||||||
|
try {
|
||||||
|
//move to the user request offset in the stream
|
||||||
|
long curOffset = localFileHandle.getFilePointer();
|
||||||
|
if (curOffset != offset) {
|
||||||
|
localFileHandle.seek(offset);
|
||||||
|
}
|
||||||
|
//note, we are always writing at 0 offset of user buffer
|
||||||
|
return localFileHandle.read(buf, 0, (int) len);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new TskCoreException(MessageFormat.format("An exception occurred while reading offset: {0}, length {1} of file", offset, len), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -337,6 +337,8 @@
|
|||||||
<package>org.apache.log.output.net</package>
|
<package>org.apache.log.output.net</package>
|
||||||
<package>org.apache.log.util</package>
|
<package>org.apache.log.util</package>
|
||||||
<package>org.apache.commons.text</package>
|
<package>org.apache.commons.text</package>
|
||||||
|
<package>org.apache.commons.text.lookup</package>
|
||||||
|
<package>org.apache.commons.text.matcher</package>
|
||||||
<package>org.apache.commons.validator.routines</package>
|
<package>org.apache.commons.validator.routines</package>
|
||||||
<package>org.apache.commons.validator.routines.checkdigit</package>
|
<package>org.apache.commons.validator.routines.checkdigit</package>
|
||||||
<package>org.apache.log4j</package>
|
<package>org.apache.log4j</package>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user