diff --git a/.gitignore b/.gitignore index 440f10c0..92c8b970 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ *.py[cod] +# Pycharm ide library +.idea + *.swp # C extensions diff --git a/volatility/commands.py b/volatility/commands.py index 6b8b82b0..4dca0d8f 100644 --- a/volatility/commands.py +++ b/volatility/commands.py @@ -22,6 +22,7 @@ import volatility.debug as debug import volatility.fmtspec as fmtspec import volatility.obj as obj import volatility.registry as registry +import volatility.renderers as renderers import volatility.addrspace as addrspace class Command(object): @@ -38,7 +39,7 @@ class Command(object): def __init__(self, config, *_args, **_kwargs): """ Constructor uses args as an initializer. It creates an instance - of OptionParser, populates the options, and finally parses the + of OptionParser, populates the options, and finally parses the command line. Options are stored in the self.opts attribute. """ self._config = config @@ -88,7 +89,7 @@ class Command(object): """ Executes the plugin command.""" # Check we can support the plugins profs = registry.get_plugin_classes(obj.Profile) - # force user to give a profile if a plugin + # force user to give a profile if a plugin # other than kdbgscan or imageinfo are given: if self.__class__.__name__.lower() in ["kdbgscan", "imageinfo"] and self._config.PROFILE == None: self._config.update("PROFILE", "WinXPSP2x86") @@ -107,7 +108,7 @@ class Command(object): function_name = "render_{0}".format(self._config.OUTPUT) if self._config.OUTPUT_FILE: outfd = open(self._config.OUTPUT_FILE, 'w') - # TODO: We should probably check that this won't blat over an existing file + # TODO: We should probably check that this won't blat over an existing file else: outfd = sys.stdout @@ -217,3 +218,11 @@ class Command(object): result = self._elide(("{0:" + spec.to_string() + "}").format(args[index]), spec.minwidth) reslist.append(result) outfd.write(self.tablesep.join(reslist) + "\n") + + def render_text(self, outfd, data): + if not hasattr(self, "unified_output"): + raise NotImplementedError("Render text using the unified output format has not been implemented for this plugin.") + output = self.unified_output(data) + + if isinstance(output, renderers.TreeGrid): + pass diff --git a/volatility/renderers/__init__.py b/volatility/renderers/__init__.py new file mode 100644 index 00000000..7a2434f3 --- /dev/null +++ b/volatility/renderers/__init__.py @@ -0,0 +1,123 @@ +"""Renderers + +Renderers display the unified output format in some manner (be it text or file or graphical output""" + +import collections +from volatility import validity, fmtspec + + +class TreeRow(validity.ValidityRoutines): + """Class providing the interface for an individual Row of the TreeGrid""" + + def __init__(self, treegrid, values): + self.type_check(treegrid, TreeGrid) + if not isinstance(self, TreeGrid): + self.type_check(values, list) + treegrid.validate_values(values) + self._treegrid = treegrid + self._children = [] + self._values = values + + def add_child(self, child): + """Appends a child to the current Row""" + self.type_check(child, TreeRow) + self._children += [child] + + def insert_child(self, child, position): + """Inserts a child at a specific position in the current Row""" + self.type_check(child, TreeRow) + self._children = self._children[:position] + [child] + self._children[:position] + + def clear(self): + """Removes all children from this row + + :rtype : None + """ + self._children = [] + + @property + def values(self): + """The individual cell values of the row""" + return self._values + + @property + def children(self): + """Returns an iterator of the children of the current row + + :rtype : iterator of TreeRows + """ + for child in self._children: + yield child + + def iterator(self, level = 0): + """Returns an iterator of all rows with their depths + + :type level: int + :param level: Indicates the depth of the current iterator + """ + yield (level, self) + for child in self.children: + for grandchild in child.iterator(level + 1): + yield grandchild + + +Column = collections.namedtuple('Column', ['index', 'name', 'type', 'format']) + + +class TreeGrid(TreeRow): + """Class providing the interface for a TreeGrid (which contains TreeRows)""" + + simple_types = set((int, str, float, bytes)) + + def __init__(self, columns): + """Constructs a TreeGrid object using a specific set of columns + + The TreeGrid itself is a root element, that can have children but no values. + The format_hint is a suggestion to the renderer as to how the field should be portrayed as a string, + but it should be noted that the renderer is not under obligation to use it. + + :param columns: A list of column tuples made up of (name, type and format_hint). + """ + self.type_check(columns, list) + converted_columns = [] + for (name, column_type, column_format) in columns: + is_simple_type = False + for stype in self.simple_types: + is_simple_type = is_simple_type or issubclass(column_type, stype) + if not is_simple_type: + raise TypeError("Column " + name + "'s type " + column_type.__class__.__name__ + + " is not a simple type") + if isinstance(column_format, str): + column_format = fmtspec.FormatSpecification.from_specification(column_format) + if not (column_format is None or isinstance(column_format, fmtspec.FormatSpecification)): + raise TypeError( + "Column " + name + "'s format " + repr(column_format) + " is not an accepted formatter.") + converted_columns.append(Column(len(converted_columns), name, column_type, column_format)) + self._columns = converted_columns + + # We can use the special type None because we're the top level node without values + TreeRow.__init__(self, self, None) + + @property + def columns(self): + """Returns list of tuples of (name, type and format_hint)""" + for column in self._columns: + yield column + + def validate_values(self, values): + """Takes a list of values and verified them against the column types""" + if len(values) != len(self._columns): + raise ValueError("The length of the values provided does not match the number of columns.") + for column in self._columns: + if not isinstance(values[column.index], column.type): + raise TypeError("Column type " + str(column.index) + " is incorrect.") + + def iterator(self, level = 0): + """Returns an iterator of all rows with their depths + + :type level: int + :param level: Indicates the depth of the current iterator + """ + for child in self.children: + for grandchild in child.iterator(level + 1): + yield grandchild diff --git a/volatility/renderers/basic.py b/volatility/renderers/basic.py new file mode 100644 index 00000000..3b4e0479 --- /dev/null +++ b/volatility/renderers/basic.py @@ -0,0 +1,53 @@ +__author__ = 'mike' + +import sys + +from volatility.framework.interfaces import renderers as interface +from volatility.framework import renderers + + +class TextRenderer(interface.Renderer): + def get_render_options(self): + # FIXME: Fill in the docstring and provide render_options + pass + + def __init__(self, options): + interface.Renderer.__init__(self, options) + self._options = options + self._column_widths = [] + + def render(self, grid): + """Renders a text grid based on the contents of each element""" + self.type_check(grid, renderers.TreeGrid) + + # FIXME: Separator should come from options + sep = " | " + + max_level = -1 + column_maximum_widths = [max(len(column.name), column.format.width) for column in grid.columns] + for (level, row) in grid.iterator(): + max_level = max(max_level, level) + column_maximum_widths = [max(column_maximum_widths[column.index], len( + ("{0:" + column.format.to_string() + "}").format(row.values[column.index]))) for column in grid.columns] + + for column in grid.columns: + column.format.width = column_maximum_widths[column.index] + column.format.fill = ' ' + # column.format.align = '<' + + # Run through the values and determine their maximum lengths, perhaps build up a two dimensional array + # Then print out the headers and the values at their appropriate spacings + # Potentially warn if the output is likely to be longer than the display area. + + headers = [("{0:" + renderers.FormatSpecification(width = column_maximum_widths[column.index], fill = ' ', + align = '^').to_string() + "}").format(column.name) for column + in + grid.columns] + print(sep.join(headers)) + + for (level, row) in grid.iterator(): + row_text = [] + for column in grid.columns: + row_text.append(("{:" + column.format.to_string() + "}").format(row.values[column.index])) + line = sep.join(row_text) + sys.stdout.write(line + "\n") diff --git a/volatility/validity.py b/volatility/validity.py new file mode 100644 index 00000000..219673e7 --- /dev/null +++ b/volatility/validity.py @@ -0,0 +1,39 @@ +""" +Created on 4 May 2013 + +@author: mike +""" + + +class ValidityRoutines(object): + """Class to hold all validation routines, such as type checking""" + + def type_check(self, value, valid_type): + """Checks that value is an instance of valid_type, and returns value if it is, or throws a TypeError otherwise + + :param value: The value of which to validate the type + :type value: object + :param valid_type: The type against which to validate + :type valid_type: type + """ + assert isinstance(value, valid_type), self.__class__.__name__ + " expected " + \ + valid_type.__name__ + ", not " + type(value).__name__ + return value + + def class_check(self, klass, valid_class): + """Checks that class is an instance of valid_class, and returns klass if it is, or throws a TypeError otherwise + + :param klass: Class to validate + :type klass: class + :param valid_class: Valid class against which to check class validity + :type valid_class: class + """ + assert issubclass(klass, valid_class), self.__class__.__name__ + " expected " + \ + valid_class.__name__ + ", not " + klass.__name__ + + def confirm(self, assertion, error): + """Acts like an assertion, but will not be disabled when __debug__ is disabled""" + if not assertion: + if error is None: + error = "An unspecified Assertion was not met in " + self.__class__.__name__ + raise AssertionError(error)