/* * Autopsy Forensic Browser * * Copyright 2014 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.interestingitems; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.regex.Pattern; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.TskData; /** * A collection of set membership rules that define an interesting files set. * The rules are independent, i.e., if any rule is satisfied by a file, the file * belongs to the set. * * Interesting files set definition objects are immutable, so they may be safely * published to multiple threads. */ final class FilesSet implements Serializable { private static final long serialVersionUID = 1L; private final String name; private final String description; private final boolean ignoreKnownFiles; private final Map<String, Rule> rules = new HashMap<>(); /** * Constructs an interesting files set. * * @param name The name of the set. * @param description A description of the set, may be null. * @param ignoreKnownFiles Whether or not to exclude known files from the * set. * @param rules The rules that define the set. May be null, but a * set with no rules is the empty set. */ FilesSet(String name, String description, boolean ignoreKnownFiles, Map<String, Rule> rules) { if ((name == null) || (name.isEmpty())) { throw new IllegalArgumentException("Interesting files set name cannot be null or empty"); } this.name = name; this.description = (description != null ? description : ""); this.ignoreKnownFiles = ignoreKnownFiles; if (rules != null) { this.rules.putAll(rules); } } /** * Gets the name of this interesting files set. * * @return A name string. */ String getName() { return this.name; } /** * Gets the description of this interesting files set. * * @return A description string, possibly the empty string. */ String getDescription() { return this.description; } /** * Returns whether or not this interesting files set ignores known files, * i.e., files marked as known by a look up in a known files hash set such * as the National Software Reference Library (NSRL). Note that the * interesting files set does not do hash set look ups; it simply queries * the known status of the files when testing them for set membership. * * @return True if known files are ignored, false otherwise. */ boolean ignoresKnownFiles() { return this.ignoreKnownFiles; } /** * Gets a copy of the set membership rules of this interesting files set. * * @return A map of set membership rule names to rules, possibly empty. */ Map<String, Rule> getRules() { return new HashMap<>(this.rules); } /** * Determines whether a file is a member of this interesting files set. * * @param file A file to test for set membership. * * @return The name of the first set membership rule satisfied by the file, * will be null if the file does not belong to the set. */ String fileIsMemberOf(AbstractFile file) { if ((this.ignoreKnownFiles) && (file.getKnown() == TskData.FileKnown.KNOWN)) { return null; } for (Rule rule : rules.values()) { if (rule.isSatisfied(file)) { return rule.getName(); } } return null; } @Override public String toString() { // This override is designed to provide a display name for use with // javax.swing.DefaultListModel<E>. return this.name; } /** * A set membership rule for an interesting files set. The immutability of a * rule object allows it to be safely published to multiple threads. */ static class Rule implements Serializable { private static final long serialVersionUID = 1L; private final String uuid; private final String ruleName; private final FileNameCondition fileNameCondition; private final MetaTypeCondition metaTypeCondition; private final ParentPathCondition pathCondition; private final MimeTypeCondition mimeTypeCondition; private final FileSizeCondition fileSizeCondition; private final List<FileAttributeCondition> conditions = new ArrayList<>(); /** * Construct an interesting files set membership rule. * * @param ruleName The name of the rule. Can be empty string. * @param fileNameCondition A file name condition, may be null. * @param metaTypeCondition A file meta-type condition. * @param pathCondition A file path condition, may be null. * @param mimeTypeCondition A file mime type condition, may be null. * @param fileSizeCondition A file size condition, may be null. */ Rule(String ruleName, FileNameCondition fileNameCondition, MetaTypeCondition metaTypeCondition, ParentPathCondition pathCondition, MimeTypeCondition mimeTypeCondition, FileSizeCondition fileSizeCondition) { // since ruleName is optional, ruleUUID can be used to uniquely identify a rule. this.uuid = UUID.randomUUID().toString(); if (metaTypeCondition == null) { throw new IllegalArgumentException("Interesting files set rule meta-type condition cannot be null"); } if (pathCondition == null && fileNameCondition == null && mimeTypeCondition == null && fileSizeCondition == null) { throw new IllegalArgumentException("Must have at least one condition on rule."); } this.ruleName = ruleName; /* * The rules are evaluated in the order added. MetaType check is * fastest, so do it first */ this.metaTypeCondition = metaTypeCondition; this.conditions.add(this.metaTypeCondition); this.fileSizeCondition = fileSizeCondition; if (this.fileSizeCondition != null) { this.conditions.add(this.fileSizeCondition); } this.fileNameCondition = fileNameCondition; if (this.fileNameCondition != null) { this.conditions.add(fileNameCondition); } this.mimeTypeCondition = mimeTypeCondition; if (this.mimeTypeCondition != null) { this.conditions.add(mimeTypeCondition); } this.pathCondition = pathCondition; if (this.pathCondition != null) { this.conditions.add(this.pathCondition); } } /** * Get the name of the rule. * * @return A name string. */ String getName() { return ruleName; } /** * Get the file name condition for the rule. * * @return A file name condition. Can be null. */ FileNameCondition getFileNameCondition() { return this.fileNameCondition; } /** * Get the meta-type condition for the rule. * * @return A meta-type condition. Can be null. */ MetaTypeCondition getMetaTypeCondition() { return this.metaTypeCondition; } /** * Get the path condition for the rule. * * @return A path condition, may be null. */ ParentPathCondition getPathCondition() { return this.pathCondition; } /** * Determines whether or not a file satisfies the rule. * * @param file The file to test. * * @return True if the rule is satisfied, false otherwise. */ boolean isSatisfied(AbstractFile file) { for (FileAttributeCondition condition : conditions) { if (!condition.passes(file)) { return false; } } return true; } @Override public String toString() { // This override is designed to provide a display name for use with // javax.swing.DefaultListModel<E>. if (fileNameCondition != null) { return this.ruleName + " (" + fileNameCondition.getTextToMatch() + ")"; } else if (this.pathCondition != null) { return this.ruleName + " (" + pathCondition.getTextToMatch() + ")"; } else if (this.mimeTypeCondition != null) { return this.ruleName + " (" + mimeTypeCondition.getMimeType() + ")"; } else if (this.fileSizeCondition != null) { return this.ruleName + " (" + fileSizeCondition.getComparator().getSymbol() + " " + fileSizeCondition.getSizeValue() + " " + fileSizeCondition.getUnit().getName() + ")"; } else { return this.ruleName + " ()"; } } /** * @return the ruleUUID */ public String getUuid() { return this.uuid; } /** * @return the mime type condition. Can be null. */ MimeTypeCondition getMimeTypeCondition() { return mimeTypeCondition; } /** * @return the file size condition. Can be null. */ FileSizeCondition getFileSizeCondition() { return fileSizeCondition; } /** * An interface for the file attribute conditions of which interesting * files set membership rules are composed. */ static interface FileAttributeCondition extends Serializable { /** * Tests whether or not a file satisfies the condition. * * @param file The file to test. * * @return True if the file passes the test, false otherwise. */ boolean passes(AbstractFile file); } /** * A class for checking files based upon their MIME types. */ static final class MimeTypeCondition implements FileAttributeCondition { private static final long serialVersionUID = 1L; private final String mimeType; /** * Constructs a MimeTypeCondition * * @param mimeType The mime type to condition for */ MimeTypeCondition(String mimeType) { this.mimeType = mimeType; } @Override public boolean passes(AbstractFile file) { return this.mimeType.equals(file.getMIMEType()); } /** * Gets the mime type that is being checked * * @return the mime type */ String getMimeType() { return this.mimeType; } } /** * A class for checking whether a file's size is within the * specifications given (i.e. < N Bytes). */ static final class FileSizeCondition implements FileAttributeCondition { private static final long serialVersionUID = 1L; /** * Represents a comparison item for file size */ static enum COMPARATOR { LESS_THAN("<"), LESS_THAN_EQUAL("≤"), EQUAL("="), GREATER_THAN(">"), GREATER_THAN_EQUAL("≥"); private String symbol; COMPARATOR(String symbol) { this.symbol = symbol; } public static COMPARATOR fromSymbol(String symbol) { if (symbol.equals("<=") || symbol.equals("≤")) { return LESS_THAN_EQUAL; } else if (symbol.equals("<")) { return LESS_THAN; } else if (symbol.equals("==") || symbol.equals("=")) { return EQUAL; } else if (symbol.equals(">")) { return GREATER_THAN; } else if (symbol.equals(">=") || symbol.equals("≥")) { return GREATER_THAN_EQUAL; } else { throw new IllegalArgumentException("Invalid symbol"); } } /** * @return the symbol */ public String getSymbol() { return symbol; } } /** * Represents the units of size */ static enum SIZE_UNIT { BYTE(1, "Bytes"), KILOBYTE(1024, "Kilobytes"), MEGABYTE(1024 * 1024, "Megabytes"), GIGABYTE(1024 * 1024 * 1024, "Gigabytes"); private long size; private String name; private SIZE_UNIT(long size, String name) { this.size = size; this.name = name; } public long getSize() { return this.size; } public static SIZE_UNIT fromName(String name) { for (SIZE_UNIT unit : SIZE_UNIT.values()) { if (unit.getName().equals(name)) { return unit; } } throw new IllegalArgumentException("Invalid name for size unit."); } /** * @return the name */ public String getName() { return name; } } private final COMPARATOR comparator; private final SIZE_UNIT unit; private final int sizeValue; FileSizeCondition(COMPARATOR comparator, SIZE_UNIT unit, int sizeValue) { this.comparator = comparator; this.unit = unit; this.sizeValue = sizeValue; } /** * Gets the comparator of this condition * * @return the comparator */ COMPARATOR getComparator() { return comparator; } /** * Gets the unit for the size of this condition * * @return the unit */ SIZE_UNIT getUnit() { return unit; } /** * Gets the size value of this condition * * @return the size value */ int getSizeValue() { return sizeValue; } @Override public boolean passes(AbstractFile file) { long fileSize = file.getSize(); long conditionSize = this.getUnit().getSize() * this.getSizeValue(); switch (this.getComparator()) { case GREATER_THAN: return fileSize > conditionSize; case GREATER_THAN_EQUAL: return fileSize >= conditionSize; case LESS_THAN_EQUAL: return fileSize <= conditionSize; case LESS_THAN: return fileSize < conditionSize; default: return fileSize == conditionSize; } } } /** * A file meta-type condition for an interesting files set membership * rule. The immutability of a meta-type condition object allows it to * be safely published to multiple threads. */ static final class MetaTypeCondition implements FileAttributeCondition { private static final long serialVersionUID = 1L; enum Type { FILES, DIRECTORIES, FILES_AND_DIRECTORIES } private final Type type; /** * Construct a meta-type condition. * * @param metaType The meta-type to match, must. */ MetaTypeCondition(Type type) { this.type = type; } @Override public boolean passes(AbstractFile file) { switch (this.type) { case FILES: return file.getMetaType() == TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG; case DIRECTORIES: return file.getMetaType() == TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR; default: return file.getMetaType() == TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG || file.getMetaType() == TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR; } } /** * Gets the meta-type the condition matches. * * @return A member of the MetaTypeCondition.Type enumeration. */ Type getMetaType() { return this.type; } } /** * An interface for file attribute conditions that do textual matching. */ static interface TextCondition extends FileAttributeCondition { /** * Gets the text the condition matches. * * @return The text. */ String getTextToMatch(); /** * Queries whether or not the text the condition matches is a * regular expression. * * @return True if the text to be matched is a regular expression, * false otherwise. */ boolean isRegex(); /** * Determines whether a string of text matches the condition. * * @param textToMatch The text string. * * @return True if the text matches, false otherwise. */ boolean textMatches(String textToMatch); } /** * An abstract base class for file attribute conditions that do textual * matching. */ private static abstract class AbstractTextCondition implements TextCondition { private final TextMatcher textMatcher; /** * Construct a case-insensitive text condition. * * @param text The text to be matched. */ AbstractTextCondition(String text, Boolean partialMatch) { if (partialMatch) { this.textMatcher = new FilesSet.Rule.CaseInsensitivePartialStringComparisionMatcher(text); } else { this.textMatcher = new FilesSet.Rule.CaseInsensitiveStringComparisionMatcher(text); } } /** * Construct a regular expression text condition. * * @param regex The regular expression to be matched. */ AbstractTextCondition(Pattern regex) { this.textMatcher = new FilesSet.Rule.RegexMatcher(regex); } /** * Get the text the condition matches. * * @return The text. */ @Override public String getTextToMatch() { return this.textMatcher.getTextToMatch(); } /** * Queries whether or not the text the condition matches is a * regular expression. * * @return True if the text to be matched is a regular expression, * false otherwise. */ @Override public boolean isRegex() { return this.textMatcher.isRegex(); } /** * Determines whether a string of text matches the condition. * * @param textToMatch The text string. * * @return True if the text matches, false otherwise. */ @Override public boolean textMatches(String textToMatch) { return this.textMatcher.textMatches(textToMatch); } @Override public abstract boolean passes(AbstractFile file); } /** * A file path condition for an interesting files set membership rule. * The immutability of a path condition object allows it to be safely * published to multiple threads. */ static final class ParentPathCondition extends AbstractTextCondition { private static final long serialVersionUID = 1L; /** * Construct a case-insensitive file path condition. * * @param path The path to be matched. */ ParentPathCondition(String path) { super(path, true); } /** * Construct a file path regular expression condition. * * @param path The path regular expression to be matched. */ ParentPathCondition(Pattern path) { super(path); } @Override public boolean passes(AbstractFile file) { return this.textMatches(file.getParentPath() + "/"); } } /** * A "tagging" interface to group name and extension conditions * separately from path conditions for type safety when constructing * rules. */ static interface FileNameCondition extends TextCondition { } /** * A file name condition for an interesting files set membership rule. * The immutability of a file name condition object allows it to be * safely published to multiple threads. */ static final class FullNameCondition extends AbstractTextCondition implements FileNameCondition { private static final long serialVersionUID = 1L; /** * Construct a case-insensitive full file name condition. * * @param name The file name to be matched. */ FullNameCondition(String name) { super(name, false); } /** * Construct a full file name regular expression condition. * * @param name The file name regular expression to be matched. */ FullNameCondition(Pattern name) { super(name); } @Override public boolean passes(AbstractFile file) { return this.textMatches(file.getName()); } } /** * A file name extension condition for an interesting files set * membership rule. The immutability of a file name extension condition * object allows it to be safely published to multiple threads. */ static final class ExtensionCondition extends AbstractTextCondition implements FileNameCondition { private static final long serialVersionUID = 1L; /** * Construct a case-insensitive file name extension condition. * * @param extension The file name extension to be matched. */ ExtensionCondition(String extension) { // If there is a leading ".", strip it since // AbstractFile.getFileNameExtension() returns just the // extension chars and not the dot. super(extension.startsWith(".") ? extension.substring(1) : extension, false); } /** * Construct a file name extension regular expression condition. * * @param extension The file name extension regular expression to be * matched. */ ExtensionCondition(Pattern extension) { super(extension.pattern(), false); } @Override public boolean passes(AbstractFile file) { return this.textMatches(file.getNameExtension()); } } /** * An interface for objects that do textual matches, used to compose a * text condition. */ private static interface TextMatcher extends Serializable { /** * Get the text the matcher examines. * * @return The text. */ String getTextToMatch(); /** * Queries whether or not the text the matcher examines is a regular * expression. * * @return True if the text to be matched is a regular expression, * false otherwise. */ boolean isRegex(); /** * Determines whether a string of text is matched. * * @param subject The text string. * * @return True if the text matches, false otherwise. */ boolean textMatches(String subject); } /** * A text matcher that does a case-insensitive string comparison. */ private static class CaseInsensitiveStringComparisionMatcher implements TextMatcher { private static final long serialVersionUID = 1L; private final String textToMatch; /** * Construct a text matcher that does a case-insensitive string * comparison. * * @param textToMatch The text to match. */ CaseInsensitiveStringComparisionMatcher(String textToMatch) { this.textToMatch = textToMatch; } @Override public String getTextToMatch() { return this.textToMatch; } @Override public boolean isRegex() { return false; } @Override public boolean textMatches(String subject) { return subject.equalsIgnoreCase(textToMatch); } } /** * A text matcher that does a case-insensitive string comparison. */ private static class CaseInsensitivePartialStringComparisionMatcher implements TextMatcher { private static final long serialVersionUID = 1L; private final String textToMatch; private final Pattern pattern; /** * Construct a text matcher that does a case-insensitive string * comparison. * * @param textToMatch The text to match. */ CaseInsensitivePartialStringComparisionMatcher(String textToMatch) { this.textToMatch = textToMatch; this.pattern = Pattern.compile(Pattern.quote(textToMatch), Pattern.CASE_INSENSITIVE); } @Override public String getTextToMatch() { return this.textToMatch; } @Override public boolean isRegex() { return false; } @Override public boolean textMatches(String subject) { return pattern.matcher(subject).find(); } } /** * A text matcher that does regular expression matching. */ private static class RegexMatcher implements TextMatcher { private static final long serialVersionUID = 1L; private final Pattern regex; /** * Construct a text matcher that does a regular expression * comparison. * * @param regex The regular expression to match. */ RegexMatcher(Pattern regex) { this.regex = regex; } @Override public String getTextToMatch() { return this.regex.pattern(); } @Override public boolean isRegex() { return true; } @Override public boolean textMatches(String subject) { // A single match is sufficient. return this.regex.matcher(subject).find(); } } } }