/* * 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.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Observable; import java.util.logging.Level; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import org.openide.util.io.NbObjectInputStream; import org.openide.util.io.NbObjectOutputStream; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.coreutils.XMLUtil; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; /** * Provides access to interesting item definitions persisted to disk. Clients * receive copies of the most recent interesting item definitions via * synchronized methods, allowing the definitions to be safely published to * multiple threads. */ final class InterestingItemDefsManager extends Observable { private static final List<String> ILLEGAL_FILE_NAME_CHARS = Collections.unmodifiableList(new ArrayList<>(Arrays.asList("\\", "/", ":", "*", "?", "\"", "<", ">"))); private static final List<String> ILLEGAL_FILE_PATH_CHARS = Collections.unmodifiableList(new ArrayList<>(Arrays.asList("\\", ":", "*", "?", "\"", "<", ">"))); private static final String LEGACY_FILES_SET_DEFS_FILE_NAME = "InterestingFilesSetDefs.xml"; //NON-NLS private static final String INTERESTING_FILES_SET_DEFS_SERIALIZATION_NAME = "InterestingFileSets.settings"; private static final String INTERESTING_FILES_SET_DEFS_SERIALIZATION_PATH = PlatformUtil.getUserConfigDirectory() + File.separator + INTERESTING_FILES_SET_DEFS_SERIALIZATION_NAME; private static final String LEGACY_FILE_SET_DEFS_PATH = PlatformUtil.getUserConfigDirectory() + File.separator + LEGACY_FILES_SET_DEFS_FILE_NAME; private static InterestingItemDefsManager instance; /** * Gets the interesting item definitions manager singleton. */ synchronized static InterestingItemDefsManager getInstance() { if (instance == null) { instance = new InterestingItemDefsManager(); } return instance; } /** * Gets the set of chars deemed to be illegal in file names (Windows). * * @return A list of characters. */ static List<String> getIllegalFileNameChars() { return InterestingItemDefsManager.ILLEGAL_FILE_NAME_CHARS; } /** * Gets the set of chars deemed to be illegal in file path * (SleuthKit/Windows). * * @return A list of characters. */ static List<String> getIllegalFilePathChars() { return InterestingItemDefsManager.ILLEGAL_FILE_PATH_CHARS; } /** * Gets a copy of the current interesting files set definitions. * * @return A map of interesting files set names to interesting file sets, * possibly empty. */ synchronized Map<String, FilesSet> getInterestingFilesSets() throws InterestingItemDefsManagerException { return FilesSetXML.readDefinitionsFile(LEGACY_FILE_SET_DEFS_PATH); } /** * Sets the current interesting file sets definitions, replacing any * previous definitions. * * @param filesSets A mapping of interesting files set names to files sets, * used to enforce unique files set names. */ synchronized void setInterestingFilesSets(Map<String, FilesSet> filesSets) throws InterestingItemDefsManagerException { FilesSetXML.writeDefinitionsFile(INTERESTING_FILES_SET_DEFS_SERIALIZATION_PATH, filesSets); this.setChanged(); this.notifyObservers(); } /** * Reads and writes interesting files set definitions to and from disk in * XML format. */ private final static class FilesSetXML { private static final Logger logger = Logger.getLogger(FilesSetXML.class.getName()); private static final String XML_ENCODING = "UTF-8"; //NON-NLS private static final List<String> illegalFileNameChars = InterestingItemDefsManager.getIllegalFileNameChars(); // The following tags and attributes are identical to those used in the // TSK Framework interesting files set definitions file schema. private static final String FILE_SETS_ROOT_TAG = "INTERESTING_FILE_SETS"; //NON-NLS private static final String FILE_SET_TAG = "INTERESTING_FILE_SET"; //NON-NLS private static final String NAME_RULE_TAG = "NAME"; //NON-NLS private static final String EXTENSION_RULE_TAG = "EXTENSION"; //NON-NLS private static final String NAME_ATTR = "name"; //NON-NLS private static final String RULE_UUID_ATTR = "ruleUUID"; //NON-NLS private static final String DESC_ATTR = "description"; //NON-NLS private static final String IGNORE_KNOWN_FILES_ATTR = "ignoreKnown"; //NON-NLS private static final String TYPE_FILTER_ATTR = "typeFilter"; //NON-NLS private static final String PATH_FILTER_ATTR = "pathFilter"; //NON-NLS private static final String TYPE_FILTER_VALUE_FILES = "file"; //NON-NLS private static final String TYPE_FILTER_VALUE_DIRS = "dir"; //NON-NLS private static final String REGEX_ATTR = "regex"; //NON-NLS private static final String PATH_REGEX_ATTR = "pathRegex"; //NON-NLS private static final String TYPE_FILTER_VALUE_FILES_AND_DIRS = "files_and_dirs"; //NON-NLS private static final String UNNAMED_LEGACY_RULE_PREFIX = "Unnamed Rule "; // NON-NLS private static int unnamedLegacyRuleCounter; /** * Reads interesting file set definitions from an XML file. * * @param filePath Path of the set definitions file as a string. * * @return The set definitions in a map of set names to sets. */ // Note: This method takes a file path to support the possibility of // multiple intersting files set definition files, e.g., one for // definitions that ship with Autopsy and one for user definitions. static Map<String, FilesSet> readDefinitionsFile(String filePath) throws InterestingItemDefsManagerException { Map<String, FilesSet> filesSets = readSerializedDefinitions(); if (!filesSets.isEmpty()) { return filesSets; } // Check if the legacy xml file exists. File defsFile = new File(filePath); if (!defsFile.exists()) { return filesSets; } // Check if the file can be read. if (!defsFile.canRead()) { logger.log(Level.SEVERE, "Interesting file sets definition file at {0} exists, but cannot be read", filePath); // NON-NLS return filesSets; } // Parse the XML in the file. Document doc = XMLUtil.loadDoc(FilesSetXML.class, filePath); if (doc == null) { logger.log(Level.SEVERE, "Failed to parse interesting file sets definition file at {0}", filePath); // NON-NLS return filesSets; } // Get the root element. Element root = doc.getDocumentElement(); if (root == null) { logger.log(Level.SEVERE, "Failed to get root {0} element tag of interesting file sets definition file at {1}", new Object[]{FilesSetXML.FILE_SETS_ROOT_TAG, filePath}); // NON-NLS return filesSets; } // Read in the files set definitions. NodeList setElems = root.getElementsByTagName(FILE_SET_TAG); for (int i = 0; i < setElems.getLength(); ++i) { readFilesSet((Element) setElems.item(i), filesSets, filePath); } return filesSets; } /** * Reads the definitions from the serialization file * * @return the map representing settings saved to serialization file, * empty set if the file does not exist. * * @throws InterestingItemDefsManagerException if file could not be read */ private static Map<String, FilesSet> readSerializedDefinitions() throws InterestingItemDefsManagerException { String filePath = INTERESTING_FILES_SET_DEFS_SERIALIZATION_PATH; File fileSetFile = new File(filePath); if (fileSetFile.exists()) { try { try (NbObjectInputStream in = new NbObjectInputStream(new FileInputStream(filePath))) { InterestingItemsFilesSetSettings filesSetsSettings = (InterestingItemsFilesSetSettings) in.readObject(); return filesSetsSettings.getFilesSets(); } } catch (IOException | ClassNotFoundException ex) { throw new InterestingItemDefsManagerException(String.format("Failed to read settings from %s", filePath), ex); } } else { return new HashMap<String, FilesSet>(); } } /** * Reads in an interesting files set. * * @param setElem An interesting files set XML element * @param filesSets A collection to which the set is to be added. * @param filePath The source file, used for error reporting. */ private static void readFilesSet(Element setElem, Map<String, FilesSet> filesSets, String filePath) { // The file set must have a unique name. String setName = setElem.getAttribute(FilesSetXML.NAME_ATTR); if (setName.isEmpty()) { logger.log(Level.SEVERE, "Found {0} element without required {1} attribute, ignoring malformed file set definition in interesting file sets definition file at {2}", new Object[]{FilesSetXML.FILE_SET_TAG, FilesSetXML.NAME_ATTR, filePath}); // NON-NLS return; } if (filesSets.containsKey(setName)) { logger.log(Level.SEVERE, "Found duplicate definition of set named {0} in interesting file sets definition file at {1}, discarding duplicate set", new Object[]{setName, filePath}); // NON-NLS return; } // The file set may have a description. The empty string is o.k. String description = setElem.getAttribute(FilesSetXML.DESC_ATTR); // The file set may or may not ignore known files. The default behavior // is to not ignore them. String ignoreKnown = setElem.getAttribute(FilesSetXML.IGNORE_KNOWN_FILES_ATTR); boolean ignoreKnownFiles = false; if (!ignoreKnown.isEmpty()) { ignoreKnownFiles = Boolean.parseBoolean(ignoreKnown); } // Read file name set membership rules, if any. FilesSetXML.unnamedLegacyRuleCounter = 1; Map<String, FilesSet.Rule> rules = new HashMap<>(); NodeList nameRuleElems = setElem.getElementsByTagName(FilesSetXML.NAME_RULE_TAG); for (int j = 0; j < nameRuleElems.getLength(); ++j) { Element elem = (Element) nameRuleElems.item(j); FilesSet.Rule rule = FilesSetXML.readFileNameRule(elem); if (rule != null) { if (!rules.containsKey(rule.getUuid())) { rules.put(rule.getUuid(), rule); } else { logger.log(Level.SEVERE, "Found duplicate rule {0} for set named {1} in interesting file sets definition file at {2}, discarding malformed set", new Object[]{rule.getUuid(), setName, filePath}); // NON-NLS return; } } else { logger.log(Level.SEVERE, "Found malformed rule for set named {0} in interesting file sets definition file at {1}, discarding malformed set", new Object[]{setName, filePath}); // NON-NLS return; } } // Read file extension set membership rules, if any. NodeList extRuleElems = setElem.getElementsByTagName(FilesSetXML.EXTENSION_RULE_TAG); for (int j = 0; j < extRuleElems.getLength(); ++j) { Element elem = (Element) extRuleElems.item(j); FilesSet.Rule rule = FilesSetXML.readFileExtensionRule(elem); if (rule != null) { if (!rules.containsKey(rule.getUuid())) { rules.put(rule.getUuid(), rule); } else { logger.log(Level.SEVERE, "Found duplicate rule {0} for set named {1} in interesting file sets definition file at {2}, discarding malformed set", new Object[]{rule.getUuid(), setName, filePath}); //NOI18N NON-NLS return; } } else { logger.log(Level.SEVERE, "Found malformed rule for set named {0} in interesting file sets definition file at {1}, discarding malformed set", new Object[]{setName, filePath}); //NOI18N NON-NLS return; } } // Make the files set. Note that degenerate sets with no rules are // allowed to facilitate the separation of set definition and rule // definitions. A set without rules is simply the empty set. FilesSet set = new FilesSet(setName, description, ignoreKnownFiles, rules); filesSets.put(set.getName(), set); } /** * Construct an interesting files set file name rule from the data in an * XML element. * * @param elem The file name rule XML element. * * @return A file name rule, or null if there is an error (the error is * logged). */ private static FilesSet.Rule readFileNameRule(Element elem) { String ruleName = FilesSetXML.readRuleName(elem); // The content of the rule tag is a file name condition. It may be a // regex, or it may be from a TSK Framework rule definition with a // "*" globbing char, or it may be simple text. String content = elem.getTextContent(); FilesSet.Rule.FullNameCondition nameCondition; String regex = elem.getAttribute(FilesSetXML.REGEX_ATTR); if ((!regex.isEmpty() && regex.equalsIgnoreCase("true")) || content.contains("*")) { // NON-NLS Pattern pattern = compileRegex(content); if (pattern != null) { nameCondition = new FilesSet.Rule.FullNameCondition(pattern); } else { logger.log(Level.SEVERE, "Error compiling " + FilesSetXML.NAME_RULE_TAG + " regex, ignoring malformed '{0}' rule definition", ruleName); // NON-NLS return null; } } else { for (String illegalChar : illegalFileNameChars) { if (content.contains(illegalChar)) { logger.log(Level.SEVERE, FilesSetXML.NAME_RULE_TAG + " content has illegal chars, ignoring malformed '{0}' rule definition", new Object[]{FilesSetXML.NAME_RULE_TAG, ruleName}); // NON-NLS return null; } } nameCondition = new FilesSet.Rule.FullNameCondition(content); } // Read in the type condition. FilesSet.Rule.MetaTypeCondition metaTypeCondition = FilesSetXML.readMetaTypeCondition(elem); if (metaTypeCondition == null) { // Malformed attribute. return null; } // Read in the optional path condition. Null is o.k., but if the attribute // is there, be sure it is not malformed. FilesSet.Rule.ParentPathCondition pathCondition = null; if (!elem.getAttribute(FilesSetXML.PATH_FILTER_ATTR).isEmpty() || !elem.getAttribute(FilesSetXML.PATH_REGEX_ATTR).isEmpty()) { pathCondition = FilesSetXML.readPathCondition(elem); if (pathCondition == null) { // Malformed attribute. return null; } } return new FilesSet.Rule(ruleName, nameCondition, metaTypeCondition, pathCondition, null, null); } /** * Construct an interesting files set file name extension rule from the * data in an XML element. * * @param elem The file name extension rule XML element. * * @return A file name extension rule, or null if there is an error (the * error is logged). */ private static FilesSet.Rule readFileExtensionRule(Element elem) { String ruleName = FilesSetXML.readRuleName(elem); // The content of the rule tag is a file name extension condition. It may // be a regex, or it may be from a TSK Framework rule definition // with a "*" globbing char. String content = elem.getTextContent(); FilesSet.Rule.ExtensionCondition extCondition; String regex = elem.getAttribute(FilesSetXML.REGEX_ATTR); if ((!regex.isEmpty() && regex.equalsIgnoreCase("true")) || content.contains("*")) { // NON-NLS Pattern pattern = compileRegex(content); if (pattern != null) { extCondition = new FilesSet.Rule.ExtensionCondition(pattern); } else { logger.log(Level.SEVERE, "Error compiling " + FilesSetXML.EXTENSION_RULE_TAG + " regex, ignoring malformed {0} rule definition", ruleName); // NON-NLS return null; } } else { for (String illegalChar : illegalFileNameChars) { if (content.contains(illegalChar)) { logger.log(Level.SEVERE, "{0} content has illegal chars, ignoring malformed {1} rule definition", ruleName); // NON-NLS return null; } } extCondition = new FilesSet.Rule.ExtensionCondition(content); } // The rule must have a meta-type condition, unless a TSK Framework // definitions file is being read. FilesSet.Rule.MetaTypeCondition metaTypeCondition = null; if (!elem.getAttribute(FilesSetXML.TYPE_FILTER_ATTR).isEmpty()) { metaTypeCondition = FilesSetXML.readMetaTypeCondition(elem); if (metaTypeCondition == null) { // Malformed attribute. return null; } } else { metaTypeCondition = new FilesSet.Rule.MetaTypeCondition(FilesSet.Rule.MetaTypeCondition.Type.FILES); } // The rule may have a path condition. Null is o.k., but if the attribute // is there, it must not be malformed. FilesSet.Rule.ParentPathCondition pathCondition = null; if (!elem.getAttribute(FilesSetXML.PATH_FILTER_ATTR).isEmpty() || !elem.getAttribute(FilesSetXML.PATH_REGEX_ATTR).isEmpty()) { pathCondition = FilesSetXML.readPathCondition(elem); if (pathCondition == null) { // Malformed attribute. return null; } } return new FilesSet.Rule(ruleName, extCondition, metaTypeCondition, pathCondition, null, null); } /** * Read a rule name attribute from a rule element. * * @param elem A rule element. * * @return A rule name. */ private static String readRuleName(Element elem) { // The rule must have a name. String ruleName = elem.getAttribute(FilesSetXML.NAME_ATTR); return ruleName; } /** * Attempts to compile a regular expression. * * @param regex The regular expression. * * @return A pattern object, or null if the compilation fails. */ private static Pattern compileRegex(String regex) { try { return Pattern.compile(regex); } catch (PatternSyntaxException ex) { logger.log(Level.SEVERE, "Error compiling rule regex: " + ex.getMessage(), ex); // NON-NLS return null; } } /** * Construct a meta-type condition for an interesting files set * membership rule from data in an XML element. * * @param ruleElement The XML element. * * @return The meta-type condition, or null if there is an error * (logged). */ private static FilesSet.Rule.MetaTypeCondition readMetaTypeCondition(Element ruleElement) { FilesSet.Rule.MetaTypeCondition condition = null; String conditionAttribute = ruleElement.getAttribute(FilesSetXML.TYPE_FILTER_ATTR); if (!conditionAttribute.isEmpty()) { switch (conditionAttribute) { case FilesSetXML.TYPE_FILTER_VALUE_FILES: condition = new FilesSet.Rule.MetaTypeCondition(FilesSet.Rule.MetaTypeCondition.Type.FILES); break; case FilesSetXML.TYPE_FILTER_VALUE_DIRS: condition = new FilesSet.Rule.MetaTypeCondition(FilesSet.Rule.MetaTypeCondition.Type.DIRECTORIES); break; case FilesSetXML.TYPE_FILTER_VALUE_FILES_AND_DIRS: condition = new FilesSet.Rule.MetaTypeCondition(FilesSet.Rule.MetaTypeCondition.Type.FILES_AND_DIRECTORIES); break; default: logger.log(Level.SEVERE, "Found {0} " + FilesSetXML.TYPE_FILTER_ATTR + " attribute with unrecognized value ''{0}'', ignoring malformed rule definition", conditionAttribute); // NON-NLS break; } } else { // Accept TSK Framework interesting files set definitions, // default to files. condition = new FilesSet.Rule.MetaTypeCondition(FilesSet.Rule.MetaTypeCondition.Type.FILES); } return condition; } /** * Construct a path condition for an interesting files set membership * rule from data in an XML element. * * @param ruleElement The XML element. * * @return The path condition, or null if there is an error (logged). */ private static FilesSet.Rule.ParentPathCondition readPathCondition(Element ruleElement) { FilesSet.Rule.ParentPathCondition condition = null; String path = ruleElement.getAttribute(FilesSetXML.PATH_FILTER_ATTR); String pathRegex = ruleElement.getAttribute(FilesSetXML.PATH_REGEX_ATTR); if (!pathRegex.isEmpty() && path.isEmpty()) { try { Pattern pattern = Pattern.compile(pathRegex); condition = new FilesSet.Rule.ParentPathCondition(pattern); } catch (PatternSyntaxException ex) { logger.log(Level.SEVERE, "Error compiling " + FilesSetXML.PATH_REGEX_ATTR + " regex, ignoring malformed path condition definition", ex); // NON-NLS } } else if (!path.isEmpty() && pathRegex.isEmpty()) { condition = new FilesSet.Rule.ParentPathCondition(path); } return condition; } /** * Writes interesting files set definitions to disk as an XML file, * logging any errors. * * @param filePath Path of the set definitions file as a string. * * @returns True if the definitions are written to disk, false * otherwise. */ // Note: This method takes a file path to support the possibility of // multiple intersting files set definition files, e.g., one for // definitions that ship with Autopsy and one for user definitions. static boolean writeDefinitionsFile(String filePath, Map<String, FilesSet> interestingFilesSets) throws InterestingItemDefsManagerException { try (NbObjectOutputStream out = new NbObjectOutputStream(new FileOutputStream(filePath))) { out.writeObject(new InterestingItemsFilesSetSettings(interestingFilesSets)); } catch (IOException ex) { throw new InterestingItemDefsManagerException(String.format("Failed to write settings to %s", filePath), ex); } return true; } } static class InterestingItemDefsManagerException extends Exception { InterestingItemDefsManagerException() { } InterestingItemDefsManagerException(String message) { super(message); } InterestingItemDefsManagerException(String message, Throwable cause) { super(message, cause); } InterestingItemDefsManagerException(Throwable cause) { super(cause); } } }