/*
* Autopsy Forensic Browser
*
* Copyright 2013 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.modules.stix;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardAttribute;
import org.sleuthkit.datamodel.TskCoreException;
import java.util.List;
import java.util.ArrayList;
import java.util.Date;
import java.util.TimeZone;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import org.mitre.cybox.common_2.ConditionApplicationEnum;
import org.mitre.cybox.objects.FileObjectType;
import org.mitre.cybox.objects.WindowsExecutableFileObjectType;
import org.mitre.cybox.common_2.ConditionTypeEnum;
import org.mitre.cybox.common_2.DatatypeEnum;
import org.mitre.cybox.common_2.HashType;
import org.mitre.cybox.common_2.DateTimeObjectPropertyType;
import org.mitre.cybox.common_2.StringObjectPropertyType;
import org.mitre.cybox.common_2.UnsignedLongObjectPropertyType;
/**
*
*/
class EvalFileObj extends EvaluatableObject {
private final FileObjectType obj;
public EvalFileObj(FileObjectType a_obj, String a_id, String a_spacing) {
obj = a_obj;
id = a_id;
spacing = a_spacing;
}
@Override
@SuppressWarnings("deprecation")
public synchronized ObservableResult evaluate() {
Case case1 = Case.getCurrentCase();
SleuthkitCase sleuthkitCase = case1.getSleuthkitCase();
setWarnings("");
String whereClause = "";
if (obj.getSizeInBytes() != null) {
try {
String newClause = processULongObject(obj.getSizeInBytes(), "size"); //NON-NLS
whereClause = addClause(whereClause, newClause);
} catch (TskCoreException ex) {
addWarning(ex.getLocalizedMessage());
}
}
if (obj.getFileName() != null) {
try {
String newClause = processStringObject(obj.getFileName(), "name"); //NON-NLS
whereClause = addClause(whereClause, newClause);
} catch (TskCoreException ex) {
addWarning(ex.getLocalizedMessage());
}
}
if (obj.getFileExtension() != null) {
if ((obj.getFileExtension().getCondition() == null)
|| (obj.getFileExtension().getCondition() == ConditionTypeEnum.EQUALS)) {
String newClause = "LOWER(name) LIKE LOWER(\'%" + obj.getFileExtension().getValue() + "\')"; //NON-NLS
whereClause = addClause(whereClause, newClause);
} else {
addWarning(
"Could not process condition " + obj.getFileExtension().getCondition().value() + " on file extension"); //NON-NLS
}
}
if (obj.getFilePath() != null) {
try {
String[] parts = obj.getFilePath().getValue().toString().split("##comma##"); //NON-NLS
String finalPathStr = "";
for (String filePath : parts) {
// First, we need to normalize the path
String currentFilePath = filePath;
// Remove the drive letter
if (currentFilePath.matches("^[A-Za-z]:.*")) {
currentFilePath = currentFilePath.substring(2);
}
// Change any backslashes to forward slashes
currentFilePath = currentFilePath.replace("\\", "/");
// The path needs to start with a slash
if (!currentFilePath.startsWith("/")) {
currentFilePath = "/" + currentFilePath;
}
// If the path does not end in a slash, the final part should be the file name.
if (!currentFilePath.endsWith("/")) {
int lastSlash = currentFilePath.lastIndexOf('/');
if (lastSlash >= 0) {
currentFilePath = currentFilePath.substring(0, lastSlash + 1);
}
}
// Reconstruct the path string (which may be multi-part)
if (!finalPathStr.isEmpty()) {
finalPathStr += "##comma##"; //NON-NLS
}
finalPathStr += currentFilePath;
}
String newClause = processStringObject(finalPathStr, obj.getFilePath().getCondition(),
obj.getFilePath().getApplyCondition(), "parent_path"); //NON-NLS
whereClause = addClause(whereClause, newClause);
} catch (TskCoreException ex) {
addWarning(ex.getLocalizedMessage());
}
}
if (obj.getCreatedTime() != null) {
try {
String newClause = processTimestampObject(obj.getCreatedTime(), "crtime"); //NON-NLS
whereClause = addClause(whereClause, newClause);
} catch (TskCoreException ex) {
addWarning(ex.getLocalizedMessage());
}
}
if (obj.getModifiedTime() != null) {
try {
String newClause = processTimestampObject(obj.getModifiedTime(), "mtime"); //NON-NLS
whereClause = addClause(whereClause, newClause);
} catch (TskCoreException ex) {
addWarning(ex.getLocalizedMessage());
}
}
if (obj.getAccessedTime() != null) {
try {
String newClause = processTimestampObject(obj.getAccessedTime(), "atime"); //NON-NLS
whereClause = addClause(whereClause, newClause);
} catch (TskCoreException ex) {
addWarning(ex.getLocalizedMessage());
}
}
if (obj.getHashes() != null) {
for (HashType h : obj.getHashes().getHashes()) {
if (h.getSimpleHashValue() != null) {
if (h.getType().getValue().equals("MD5")) { //NON-NLS
String newClause = "";
if (h.getSimpleHashValue().getValue().toString().toLowerCase().contains("##comma##")) { //NON-NLS
String[] parts = h.getSimpleHashValue().getValue().toString().toLowerCase().split("##comma##"); //NON-NLS
String hashList = "";
for (String s : parts) {
if (!hashList.isEmpty()) {
hashList += ", ";
}
hashList += "\'" + s + "\'";
}
newClause = "md5 IN (" + hashList + ")"; //NON-NLS
} else {
newClause = "md5=\'" + h.getSimpleHashValue().getValue().toString().toLowerCase() + "\'"; //NON-NLS
}
whereClause = addClause(whereClause, newClause);
} else {
addWarning("Could not process hash type " + h.getType().getValue().toString()); //NON-NLS
}
} else {
addWarning("Could not process non-simple hash value"); //NON-NLS
}
}
}
if (obj instanceof WindowsExecutableFileObjectType) {
WindowsExecutableFileObjectType winExe = (WindowsExecutableFileObjectType) obj;
if (winExe.getHeaders() != null) {
if (winExe.getHeaders().getFileHeader() != null) {
if (winExe.getHeaders().getFileHeader().getTimeDateStamp() != null) {
try {
String result = convertTimestampString(winExe.getHeaders().getFileHeader().getTimeDateStamp().getValue().toString());
String newClause = processNumericFields(result,
winExe.getHeaders().getFileHeader().getTimeDateStamp().getCondition(),
winExe.getHeaders().getFileHeader().getTimeDateStamp().getApplyCondition(),
"crtime"); //NON-NLS
whereClause = addClause(whereClause, newClause);
} catch (TskCoreException ex) {
addWarning(ex.getLocalizedMessage());
}
}
}
}
}
String unsupportedFields = listUnsupportedFields();
if (!unsupportedFields.isEmpty()) {
addWarning("Unsupported fields: " + unsupportedFields); //NON-NLS
}
if (whereClause.length() > 0) {
try {
List<AbstractFile> matchingFiles = sleuthkitCase.findAllFilesWhere(whereClause);
if (!matchingFiles.isEmpty()) {
if (listSecondaryFields().isEmpty()) {
List<StixArtifactData> artData = new ArrayList<StixArtifactData>();
for (AbstractFile a : matchingFiles) {
artData.add(new StixArtifactData(a, id, "FileObject")); //NON-NLS
}
return new ObservableResult(id, "FileObject: Found " + matchingFiles.size() + " matches for " + whereClause + getPrintableWarnings(), //NON-NLS
spacing, ObservableResult.ObservableState.TRUE, artData);
} else {
// We need to tag the matching files in Autopsy, so keep track of them
List<AbstractFile> secondaryHits = new ArrayList<AbstractFile>();
for (AbstractFile file : matchingFiles) {
boolean passedTests = true;
if (obj.isIsMasqueraded() != null) {
List<BlackboardArtifact> arts = file.getArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_EXT_MISMATCH_DETECTED);
boolean isMasq = false;
if (!arts.isEmpty()) {
isMasq = true;
}
if (obj.isIsMasqueraded() != isMasq) {
passedTests = false;
}
}
if (obj.getFileFormat() != null) {
String formatsFound = file.getMIMEType();
if (formatsFound != null) {
if (!(formatsFound.equalsIgnoreCase(obj.getFileFormat().getValue().toString()))) {
addWarning("Warning: Did not match File_Format field " + obj.getFileFormat().getValue().toString() //NON-NLS
+ " against " + formatsFound); //NON-NLS
}
} else {
addWarning("Warning: Did not match File_Format field " + obj.getFileFormat().getValue().toString() //NON-NLS
+ " (no file formats found)"); //NON-NLS
}
// It looks like the STIX file formats can be different than what Autopsy stores
// (mime vs. unix file), so don't kill a file based on this field not matching.
//if (!foundMatch) {
// passedTests = false;
//}
}
if (passedTests) {
secondaryHits.add(file);
}
}
if (secondaryHits.isEmpty()) {
return new ObservableResult(id, "FileObject: Found " + matchingFiles.size() + " matches for " + whereClause //NON-NLS
+ " but none for secondary tests on " + listSecondaryFields() + getPrintableWarnings(), //NON-NLS
spacing, ObservableResult.ObservableState.FALSE, null);
} else {
List<StixArtifactData> artData = new ArrayList<StixArtifactData>();
for (AbstractFile a : secondaryHits) {
artData.add(new StixArtifactData(a, id, "FileObject")); //NON-NLS
}
return new ObservableResult(id, "FileObject: Found " + secondaryHits.size() + " matches for " + whereClause //NON-NLS
+ " and secondary tests on " + listSecondaryFields() + getPrintableWarnings(), //NON-NLS
spacing, ObservableResult.ObservableState.TRUE, artData);
}
}
} else {
return new ObservableResult(id, "FileObject: Found no matches for " + whereClause + getPrintableWarnings(), //NON-NLS
spacing, ObservableResult.ObservableState.FALSE, null);
}
} catch (TskCoreException ex) {
return new ObservableResult(id, "FileObject: Exception during evaluation: " + ex.getLocalizedMessage(), //NON-NLS
spacing, ObservableResult.ObservableState.INDETERMINATE, null);
}
} else {
}
return new ObservableResult(id, "FileObject: No evaluatable fields " + getPrintableWarnings(), spacing, //NON-NLS
ObservableResult.ObservableState.INDETERMINATE, null);
}
/**
* Create a list of secondary fields. These are the ones that we only test
* on the matches for the primary fields.
*
* @return List of secondary fields
*/
private String listSecondaryFields() {
String secondaryFields = "";
if (obj.isIsMasqueraded() != null) {
secondaryFields += "is_masqueraded "; //NON-NLS
}
if (obj.getFileFormat() != null) {
secondaryFields += "File_Format "; //NON-NLS
}
return secondaryFields;
}
/**
* List unsupported fields found in the object.
*
* @return List of unsupported fields
*/
private String listUnsupportedFields() {
String unsupportedFields = "";
if (obj.isIsPacked() != null) {
unsupportedFields += "is_packed "; //NON-NLS
}
if (obj.getDevicePath() != null) {
unsupportedFields += "Device_Path "; //NON-NLS
}
if (obj.getFullPath() != null) {
unsupportedFields += "Full_Path "; //NON-NLS
}
if (obj.getMagicNumber() != null) {
unsupportedFields += "Magic_Number "; //NON-NLS
}
if (obj.getDigitalSignatures() != null) {
unsupportedFields += "Digital_Signatures "; //NON-NLS
}
if (obj.getFileAttributesList() != null) {
unsupportedFields += "File_Attributes_List "; //NON-NLS
}
if (obj.getPermissions() != null) {
unsupportedFields += "Permissions "; //NON-NLS
}
if (obj.getUserOwner() != null) {
unsupportedFields += "User_Owner "; //NON-NLS
}
if (obj.getPackerList() != null) {
unsupportedFields += "Packer_List "; //NON-NLS
}
if (obj.getPeakEntropy() != null) {
unsupportedFields += "Peak_Entropy "; //NON-NLS
}
if (obj.getSymLinks() != null) {
unsupportedFields += "Sym_Links "; //NON-NLS
}
if (obj.getByteRuns() != null) {
unsupportedFields += "Bytes_Runs "; //NON-NLS
}
if (obj.getExtractedFeatures() != null) {
unsupportedFields += "Extracted_Features "; //NON-NLS
}
if (obj.getEncryptionAlgorithm() != null) {
unsupportedFields += "Encryption_Algorithm "; //NON-NLS
}
if (obj.getDecryptionKey() != null) {
unsupportedFields += "Decryption_Key "; //NON-NLS
}
if (obj.getCompressionMethod() != null) {
unsupportedFields += "Compression_Method "; //NON-NLS
}
if (obj.getCompressionVersion() != null) {
unsupportedFields += "Compression_Version "; //NON-NLS
}
if (obj.getCompressionComment() != null) {
unsupportedFields += "Compression_Comment "; //NON-NLS
}
return unsupportedFields;
}
/**
* Convert timestamp string into a long.
*
* @param timeStr
*
* @return
*
* @throws ParseException
*/
private static long convertTimestamp(String timeStr) throws ParseException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); //NON-NLS
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); //NON-NLS
Date parsedDate = dateFormat.parse(timeStr);
Long unixTime = parsedDate.getTime() / 1000;
return unixTime;
}
/**
* Return the SQL clause for an unsigned long object. Splits into fields and
* call the more generic version of the function.
*
* @param longObj The Cybox UnsignedLong object
* @param fieldName Name of the field to test against
*
* @return SQL clause
*
* @throws TskCoreException
*/
private static String processULongObject(UnsignedLongObjectPropertyType longObj, String fieldName)
throws TskCoreException {
return processNumericFields(longObj.getValue().toString(), longObj.getCondition(),
longObj.getApplyCondition(), fieldName);
}
/**
* Return the SQL clause for a numeric object.
*
* @param valueStr Value (as string)
* @param typeCondition Cybox condition
* @param applyCondition Cybox apply_condition
* @param fieldName Name of the field to test against
*
* @return SQL clause
*
* @throws TskCoreException
*/
private static String processNumericFields(String valueStr, ConditionTypeEnum typeCondition,
ConditionApplicationEnum applyCondition, String fieldName)
throws TskCoreException {
if ((typeCondition == null)
|| ((typeCondition != ConditionTypeEnum.INCLUSIVE_BETWEEN)
&& (typeCondition != ConditionTypeEnum.EXCLUSIVE_BETWEEN))) {
String fullClause = "";
if (valueStr.isEmpty()) {
throw new TskCoreException("Empty value field"); //NON-NLS
}
String[] parts = valueStr.split("##comma##"); //NON-NLS
for (String valuePart : parts) {
String partialClause;
if ((typeCondition == null)
|| (typeCondition == ConditionTypeEnum.EQUALS)) {
partialClause = fieldName + "=" + valuePart;
} else if (typeCondition == ConditionTypeEnum.DOES_NOT_EQUAL) {
partialClause = fieldName + "!=" + valuePart;
} else if (typeCondition == ConditionTypeEnum.GREATER_THAN) {
partialClause = fieldName + ">" + valuePart;
} else if (typeCondition == ConditionTypeEnum.GREATER_THAN_OR_EQUAL) {
partialClause = fieldName + ">=" + valuePart;
} else if (typeCondition == ConditionTypeEnum.LESS_THAN) {
partialClause = fieldName + "<" + valuePart;
} else if (typeCondition == ConditionTypeEnum.LESS_THAN_OR_EQUAL) {
partialClause = fieldName + "<=" + valuePart;
} else {
throw new TskCoreException("Could not process condition " + typeCondition.value() + " on " + fieldName); //NON-NLS
}
if (fullClause.isEmpty()) {
if (parts.length > 1) {
fullClause += "( ";
}
if (applyCondition == ConditionApplicationEnum.NONE) {
fullClause += " NOT "; //NON-NLS
}
fullClause += partialClause;
} else {
if (applyCondition == ConditionApplicationEnum.ALL) {
fullClause += " AND " + partialClause; //NON-NLS
} else if (applyCondition == ConditionApplicationEnum.NONE) {
fullClause += " AND NOT " + partialClause; //NON-NLS
} else {
fullClause += " OR " + partialClause; //NON-NLS
}
}
}
if (parts.length > 1) {
fullClause += " )";
}
return fullClause;
} else {
// I don't think apply conditions make sense for these two.
if (typeCondition == ConditionTypeEnum.INCLUSIVE_BETWEEN) {
String[] parts = valueStr.split("##comma##"); //NON-NLS
if (parts.length != 2) {
throw new TskCoreException("Unexpected number of arguments in INCLUSIVE_BETWEEN on " + fieldName //NON-NLS
+ "(" + valueStr + ")");
}
return (fieldName + ">=" + parts[0] + " AND " + fieldName + "<=" + parts[1]); //NON-NLS
} else {
String[] parts = valueStr.split("##comma##"); //NON-NLS
if (parts.length != 2) {
throw new TskCoreException("Unexpected number of arguments in EXCLUSIVE_BETWEEN on " + fieldName //NON-NLS
+ "(" + valueStr + ")");
}
return (fieldName + ">" + parts[0] + " AND " + fieldName + "<" + parts[1]); //NON-NLS
}
}
}
/**
* Return the SQL clause for a String object
*
* @param stringObj The full Cybox String object
* @param fieldName Name of the field we're testing against
*
* @return SQL clause
*
* @throws TskCoreException
*/
private static String processStringObject(StringObjectPropertyType stringObj, String fieldName)
throws TskCoreException {
return processStringObject(stringObj.getValue().toString(), stringObj.getCondition(),
stringObj.getApplyCondition(), fieldName);
}
/**
* Return the SQL clause for a String object
*
* @param valueStr Value as a string
* @param condition Cybox condition
* @param applyCondition Cybox apply_condition
* @param fieldName Name of the field we're testing against
*
* @return SQL clause
*
* @throws TskCoreException
*/
public static String processStringObject(String valueStr, ConditionTypeEnum condition,
ConditionApplicationEnum applyCondition, String fieldName)
throws TskCoreException {
String fullClause = "";
String lowerFieldName = "lower(" + fieldName + ")"; //NON-NLS
if (valueStr.isEmpty()) {
throw new TskCoreException("Empty value field"); //NON-NLS
}
String[] parts = valueStr.split("##comma##"); //NON-NLS
for (String value : parts) {
String lowerValue = value.toLowerCase();
String partialClause;
if ((condition == null)
|| (condition == ConditionTypeEnum.EQUALS)) {
partialClause = lowerFieldName + "=\'" + lowerValue + "\'";
} else if (condition == ConditionTypeEnum.DOES_NOT_EQUAL) {
partialClause = lowerFieldName + " !=\'%" + lowerValue + "%\'";
} else if (condition == ConditionTypeEnum.CONTAINS) {
partialClause = lowerFieldName + " LIKE \'%" + lowerValue + "%\'"; //NON-NLS
} else if (condition == ConditionTypeEnum.DOES_NOT_CONTAIN) {
partialClause = lowerFieldName + " NOT LIKE \'%" + lowerValue + "%\'"; //NON-NLS
} else if (condition == ConditionTypeEnum.STARTS_WITH) {
partialClause = lowerFieldName + " LIKE \'" + lowerValue + "%\'"; //NON-NLS
} else if (condition == ConditionTypeEnum.ENDS_WITH) {
partialClause = lowerFieldName + " LIKE \'%" + lowerValue + "\'"; //NON-NLS
} else {
throw new TskCoreException("Could not process condition " + condition.value() + " on " + fieldName); //NON-NLS
}
if (fullClause.isEmpty()) {
if (parts.length > 1) {
fullClause += "( ";
}
if (applyCondition == ConditionApplicationEnum.NONE) {
fullClause += " NOT "; //NON-NLS
}
fullClause += partialClause;
} else {
if (applyCondition == ConditionApplicationEnum.ALL) {
fullClause += " AND " + partialClause; //NON-NLS
} else if (applyCondition == ConditionApplicationEnum.NONE) {
fullClause += " AND NOT " + partialClause; //NON-NLS
} else {
fullClause += " OR " + partialClause; //NON-NLS
}
}
}
if (parts.length > 1) {
fullClause += " )";
}
return fullClause;
}
/**
* Create the SQL clause for a timestamp object. Converts the time into a
* numeric field and then creates the clause from that.
*
* @param dateObj Cybox DateTimeObject
* @param fieldName Name of the field we're testing against
*
* @return SQL clause
*
* @throws TskCoreException
*/
private static String processTimestampObject(DateTimeObjectPropertyType dateObj, String fieldName)
throws TskCoreException {
if (DatatypeEnum.DATE_TIME == dateObj.getDatatype()) {
// Change the string into unix timestamps
String result = convertTimestampString(dateObj.getValue().toString());
return processNumericFields(result, dateObj.getCondition(), dateObj.getApplyCondition(), fieldName);
} else {
throw new TskCoreException("Found non DATE_TIME field on " + fieldName); //NON-NLS
}
}
/**
* Convert a timestamp string into a numeric one. Leave it as a string since
* that's what we get from other object types.
*
* @param timestampStr
*
* @return String version with timestamps replaced by numeric values
*
* @throws TskCoreException
*/
private static String convertTimestampString(String timestampStr)
throws TskCoreException {
try {
String result = "";
if (timestampStr.length() > 0) {
String[] parts = timestampStr.split("##comma##"); //NON-NLS
for (int i = 0; i < parts.length - 1; i++) {
long unixTime = convertTimestamp(parts[i]);
result += unixTime + "##comma##"; //NON-NLS
}
result += convertTimestamp(parts[parts.length - 1]);
}
return result;
} catch (java.text.ParseException ex) {
throw new TskCoreException("Error parsing timestamp string " + timestampStr); //NON-NLS
}
}
/**
* Add a new clause to the existing clause
*
* @param a_clause Current clause
* @param a_newClause New clause
*
* @return Full clause
*/
private static String addClause(String a_clause, String a_newClause) {
if ((a_clause == null) || a_clause.isEmpty()) {
return a_newClause;
}
return (a_clause + " AND " + a_newClause); //NON-NLS
}
}