From cf66d0b447c3eaf5bcd0fdd7fc3a5d4096450ce8 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 29 Dec 2015 22:13:10 +0000 Subject: [PATCH] Start building a dependency resolver for translation layers. --- volatility/framework/__init__.py | 11 +++ .../framework/configuration/__init__.py | 56 ++---------- .../framework/configuration/depresolver.py | 20 +++++ .../framework/interfaces/configuration.py | 85 +++---------------- volatility/framework/interfaces/layers.py | 8 +- volatility/framework/plugins/__init__.py | 11 --- 6 files changed, 56 insertions(+), 135 deletions(-) create mode 100644 volatility/framework/configuration/depresolver.py diff --git a/volatility/framework/__init__.py b/volatility/framework/__init__.py index 5d1ccc23c..2e0e5306c 100644 --- a/volatility/framework/__init__.py +++ b/volatility/framework/__init__.py @@ -1,4 +1,5 @@ """Volatility 3 framework""" +import inspect # ## # @@ -36,4 +37,14 @@ def require_version(*args): ".".join([str(x) for x in args[0:2]])) +def class_subclasses(cls): + """Returns all the (recursive) subclasses of a given class""" + if not inspect.isclass(cls): + raise TypeError(repr(cls) + " is not a class.") + for clazz in cls.__subclasses__(): + yield clazz + for return_value in class_subclasses(clazz): + yield return_value + + from volatility.framework import interfaces, symbols, layers, contexts, configuration diff --git a/volatility/framework/configuration/__init__.py b/volatility/framework/configuration/__init__.py index 6989d832c..dd3e615d2 100644 --- a/volatility/framework/configuration/__init__.py +++ b/volatility/framework/configuration/__init__.py @@ -4,15 +4,13 @@ Created on 7 May 2013 @author: mike """ -from volatility.framework.interfaces.configuration import ConfigurationSchemaNode, Configurable +from volatility.framework.interfaces.configuration import ConfigurationSchemaNode class InstanceRequirement(ConfigurationSchemaNode): instance_type = bool - def validate(self, value, _context, valid_children = None): - # Child_results can be ignored because no instance class should have children - # We don't ban child_results in case someone wants to extend this in some meaningful way + def validate(self, value, _context): if not isinstance(value, self.instance_type): raise TypeError(self.name + " input only accepts " + self.instance_type.__name__ + " type") @@ -26,7 +24,7 @@ class StringRequirement(InstanceRequirement): instance_type = str -class TranslationLayerRequirement(ConfigurationSchemaNode, Configurable): +class TranslationLayerRequirement(ConfigurationSchemaNode): """Class maintaining the limitations on what sort of address spaces are acceptable""" def __init__(self, name, description = None, default = None, @@ -40,19 +38,12 @@ class TranslationLayerRequirement(ConfigurationSchemaNode, Configurable): :return: """ ConfigurationSchemaNode.__init__(name, description, default, optional) - Configurable.__init__(self) self._layer_name = layer_name - @classmethod - def get_schema(cls): - # Runs through each of the available translation layers, determining what they're capable of, - # and adding their requirements (tagged with their class) to a Disjunction config schema node - pass - # TODO: Add requirements: acceptable OSes from the address_space information # TODO: Add requirements: acceptable arches from the available layers - def validate(self, value, context, valid_children = None): + def validate(self, value, context): """Validate that the value is a valid layer name and that the layer adheres to the requirements""" if not isinstance(value, str): raise TypeError("TranslationLayerRequirements only accepts string labels") @@ -61,8 +52,7 @@ class TranslationLayerRequirement(ConfigurationSchemaNode, Configurable): class ChoiceRequirement(ConfigurationSchemaNode): - """Allows one from a choice of strings - """ + """Allows one from a choice of strings""" def __init__(self, choices, *args, **kwargs): ConfigurationSchemaNode.__init__(self, *args, **kwargs) @@ -70,7 +60,7 @@ class ChoiceRequirement(ConfigurationSchemaNode): raise TypeError("ChoiceRequirement takes a list of strings as choices") self._choices = choices - def validate(self, value, context, valid_children = None): + def validate(self, value, context): """Validates the provided value to ensure it is one of the available choices""" if value not in self._choices: raise ValueError("Value is not within the set of available choices") @@ -85,7 +75,7 @@ class ListRequirement(ConfigurationSchemaNode): self.min_elements = min_elements self.max_elements = max_elements - def validate(self, value, context, valid_children = None): + def validate(self, value, context): """Check the types on each of the returned values and then call the element type's check for each one""" self._type_check(value, list) if not all([self._type_check(element, self.element_type) for element in value]): @@ -94,35 +84,3 @@ class ListRequirement(ConfigurationSchemaNode): raise TypeError("List option provided more or less elements than allowed.") for element in value: self.element_type.validate(element, context) - - -class DisjunctionRequirement(ConfigurationSchemaNode): - """Class allowing any of multiple requirements""" - - def __init__(self, requirements, *args, **kwargs): - # TODO: Type check requirements to ensure it's a dictionary of requirements - ConfigurationSchemaNode.__init__(self, *args, **kwargs) - for requirement in requirements: - self.add_item(requirement) - - def validate(self, value, context, valid_children = None): - if valid_children is None: - raise ValueError("A Disjunction requirement cannot exist without children") - if not any(valid_children): - raise ValueError("No valid children to support the Disjunction Requirement") - - -class ConjunctionRequirement(ConfigurationSchemaNode): - """Class requiring all of multiple requirements""" - - def __init__(self, requirements, *args, **kwargs): - # TODO: Type check requirements to ensure it's a dictionary of requirements - ConfigurationSchemaNode.__init__(self, *args, **kwargs) - for requirement in requirements: - self.add_item(requirement) - - def validate(self, value, context, valid_children = None): - if valid_children is None: - raise ValueError("A Conjunction requirement cannot exist without children") - if not all(valid_children): - raise ValueError("An invalid child prevents the Conjunction Requirement") diff --git a/volatility/framework/configuration/depresolver.py b/volatility/framework/configuration/depresolver.py new file mode 100644 index 000000000..4388ece10 --- /dev/null +++ b/volatility/framework/configuration/depresolver.py @@ -0,0 +1,20 @@ +import volatility.framework as framework +import volatility.framework.interfaces as interfaces +import volatility.framework.validity as validity + + +class TranslationLayerDependencyResolver(validity.ValidityRoutines): + def __init__(self): + # Maintain a cache of translation layers + self.layer_cache = [] + for layer_class in framework.class_subclasses(interfaces.layers.DataLayerInterface): + # TODO: Improve this hard coded list with a way for layers to say they're abstract or not + if layer_class.__name__.endswith("Interface"): + self.layer_cache.append(layer_class) + + def resolve_dependencies(self, configurable): + """Takes a configurable and produces a priority ordered tree of possible solutions to satisfy the various requirements""" + self._type_check(configurable, interfaces.configuration.Configurable) + + for requirement in configurable.get_schemas(): + pass diff --git a/volatility/framework/interfaces/configuration.py b/volatility/framework/interfaces/configuration.py index 41693dc8b..634fbbefd 100644 --- a/volatility/framework/interfaces/configuration.py +++ b/volatility/framework/interfaces/configuration.py @@ -9,22 +9,18 @@ SCHEMA_NAME_DIVIDER = "." # Design requirements: # Plugins can be queried for their requirements without instantiating them -# The context can record validated data -# Configurables can have multiple identical (but differently named) requirement trees (based off the same schema) -# Instantiated plugins can pre-populate the context with (validated) da (in order to force layer names) +# The context can record config data # Translation layer requirements can specify a layer name (or generate one if not specified) -# Still not decided whether the layer holds the name chosen (and this can be pre-populated) or if it's specified -# as part of the requirement, which then returns true/false if it could be fulfilled +# It's specified as part of the requirement, which then validates true/false if it could be fulfilled # Still need to link schema in to config values # Need to allow config values to be recovered by complete sub-path -# Configurables need to hold a hook into the config tree to get their own sub components # Non-instantiated plugin # -> Requirement schema nodes (instantiated) # (Translation layers contain all information required) -# Instantiate plugin -# -> Requirement schema nodes (instantiated) wrapped in value reader/writer/valdiator +# Dependency solver +# Attempts to fill all dependencies by traversing the various available classes to find a solution def schema_name_join(pathlist): @@ -49,7 +45,6 @@ class ConfigurationSchemaNode(validity.ValidityRoutines): self._description = description or "" self._default = default self._optional = optional - self._children = {} @property def name(self): @@ -71,80 +66,16 @@ class ConfigurationSchemaNode(validity.ValidityRoutines): """Whether the option is required for or not""" return self._optional - # Child operations - - def add_item(self, item): - """Add a child to the configuration schema""" - self._type_check(item, ConfigurationSchemaNode) - self._children[item.name] = item - - def __iter__(self): - """Iterate through all the child configuration schemas""" - return iter(self._children) - - def __getitem__(self, item): - """Returns a single child configuration schema by name""" - self._type_check(item, str) - item_split = item.split(SCHEMA_NAME_DIVIDER) - if len(item_split) > 1: - return self._children[item_split[0]][schema_name_join(item_split[1:])] - # Let namespace produce the index error if necessary - return self._children[item_split[0]] - - def __contains__(self, item): - """Determine membership""" - item_split = item.split(SCHEMA_NAME_DIVIDER) - if len(item_split) > 1: - if item_split[0] in self._children: - return schema_name_join(item_split[1:]) in self._children[item_split[0]] - else: - return False - return item in self._children - - def __len__(self): - return len(self._children) - # Validation routines @abstractmethod - def validate(self, value, context, valid_children = None): + def validate(self, value, context): """Method to validate the value stored at config_location for the configuration object against a context - This must validate its own children (so that conjunction/disjunction can work) Raises a ValueError based on whether the item is valid or not """ -class ConfigurationItem(validity.ValidityRoutines): - """Class for wrapping ConfigurationSchemaNodes to create specific configuration items""" - - def __init__(self, schema, config_location_prefix = None): - """""" - validity.ValidityRoutines.__init__(self) - self._schema = self._type_check(schema, ConfigurationSchemaNode) - if config_location_prefix is None: - config_location_prefix = "core" - self._config_location = schema_name_join( - schema_name_split(self._type_check(config_location_prefix, str)) + - [self._schema.name]) - - self._children = [] - - for child in self._schema: - self._children.append( - ConfigurationItem(child, self._config_location)) - - def valid(self, context): - child_results = [] - for child in self._children: - child_results.append(child.valid(context)) - try: - self._schema.validate(context.config[self._config_location], context, child_results) - return True - except ValueError: - return False - - class Configurable(metaclass = ABCMeta): """Class to allow objects to have requirements and populate the context config tree""" @@ -152,3 +83,9 @@ class Configurable(metaclass = ABCMeta): @abstractmethod def get_schema(cls): """Returns a list of configuration schema nodes for this object""" + return [] + + def create_configuration(self, location, context): + """Pins the configuration schemas to a location within the context's config storage""" + for requirement in self.get_schema(): + pass diff --git a/volatility/framework/interfaces/layers.py b/volatility/framework/interfaces/layers.py index 51dfe170f..b0168ad23 100644 --- a/volatility/framework/interfaces/layers.py +++ b/volatility/framework/interfaces/layers.py @@ -6,11 +6,12 @@ Created on 4 May 2013 from volatility.framework import validity, exceptions # We can't just import interfaces because we'd have a cycle going +from volatility.framework.interfaces import configuration from volatility.framework.interfaces import context as context_module from abc import ABCMeta, abstractmethod, abstractproperty -class DataLayerInterface(validity.ValidityRoutines, metaclass = ABCMeta): +class DataLayerInterface(validity.ValidityRoutines, configuration.Configurable, metaclass = ABCMeta): """A Layer that directly holds data (and does not translate it""" def __init__(self, context, name): @@ -60,6 +61,11 @@ class DataLayerInterface(validity.ValidityRoutines, metaclass = ABCMeta): (exceptions will be thrown using a DataLayer after destruction)""" pass + @abstractmethod + @classmethod + def get_schema(cls): + """Returns a list of requirements for this type of layer""" + class TranslationLayerInterface(DataLayerInterface, metaclass = ABCMeta): @abstractmethod diff --git a/volatility/framework/plugins/__init__.py b/volatility/framework/plugins/__init__.py index 3a79b78a6..65826a7bd 100644 --- a/volatility/framework/plugins/__init__.py +++ b/volatility/framework/plugins/__init__.py @@ -1,7 +1,6 @@ # TODO: Code to import all the py/pyc files available (but not both). # TODO: Code to return a none-instantiated list of plugin classes. -import inspect import logging import os import sys @@ -11,16 +10,6 @@ import volatility.plugins as plugins logger = logging.getLogger(__name__) -def class_subclasses(cls): - """Returns all the (recursive) subclasses of a given class""" - if not inspect.isclass(cls): - raise TypeError(repr(cls) + " is not a class.") - for clazz in cls.__subclasses__(): - yield clazz - for return_value in class_subclasses(clazz): - yield return_value - - def import_plugins(): """Imports all plugins present under plugins path""" if not isinstance(plugins.__path__, list):