mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-08-28 18:59:44 +02:00
Documentation: Ensure all references in the docs are accurate
This commit is contained in:
+28
-28
@@ -7,7 +7,7 @@ Volatility splits memory analysis down to several components:
|
||||
* Templates and Objects
|
||||
* Symbol Tables
|
||||
|
||||
Volatility 3 stores all of these within a :py:class:`Context <volatility.framework.interfaces.context.ContextInterface>`,
|
||||
Volatility 3 stores all of these within a :py:class:`Context <volatility3.framework.interfaces.context.ContextInterface>`,
|
||||
which acts as a container for all the various layers and tables necessary to conduct memory analysis.
|
||||
|
||||
Memory layers
|
||||
@@ -21,8 +21,8 @@ two other sources. These are typically handled by programs that process file fo
|
||||
processor, but these are all translations (either in the geometric or linguistic sense) of the original data.
|
||||
|
||||
In Volatility 3 this is represented by a directed graph, whose end nodes are
|
||||
:py:class:`DataLayers <volatility.framework.interfaces.layers.DataLayerInterface>` and whose internal nodes are
|
||||
specifically called a :py:class:`TranslationLayer <volatility.framework.interfaces.layers.TranslationLayerInterface>`.
|
||||
:py:class:`DataLayers <volatility3.framework.interfaces.layers.DataLayerInterface>` and whose internal nodes are
|
||||
specifically called a :py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>`.
|
||||
In this way, a raw memory image in the LiME file format and a page file can be
|
||||
combined to form a single Intel virtual memory layer. When requesting addresses from the Intel layer, it will use the
|
||||
Intel memory mapping algorithm, along with the address of the directory table base or page table map, to translate that
|
||||
@@ -39,17 +39,17 @@ Templates and Objects
|
||||
|
||||
Once we can address contiguous chunks of memory with a means to translate a virtual address (as seen by the programs)
|
||||
into the actual data used by the processor, we can start pulling out
|
||||
:py:class:`Objects <volatility.framework.interfaces.objects.ObjectInterface>` by taking a
|
||||
:py:class:`~volatility.framework.interfaces.objects.Template` and constructing
|
||||
it on the memory layer at a specific offset. A :py:class:`~volatility.framework.interfaces.objects.Template` contains
|
||||
:py:class:`Objects <volatility3.framework.interfaces.objects.ObjectInterface>` by taking a
|
||||
:py:class:`~volatility3.framework.interfaces.objects.Template` and constructing
|
||||
it on the memory layer at a specific offset. A :py:class:`~volatility3.framework.interfaces.objects.Template` contains
|
||||
all the information you can know about the structure of the object without actually being populated by any data.
|
||||
As such a :py:class:`~volatility.framework.interfaces.objects.Template` can tell you the size of a structure and its
|
||||
As such a :py:class:`~volatility3.framework.interfaces.objects.Template` can tell you the size of a structure and its
|
||||
members, how far into the structure a particular member lives and potentially what various values in that field would
|
||||
mean, but not what resides in a particular member.
|
||||
|
||||
Using a :py:class:`~volatility.framework.interfaces.objects.Template` on a memory layer at a particular offset, an
|
||||
:py:class:`Object <volatility.framework.interfaces.objects.ObjectInterface>` can be constructed. In Volatility 3, once an
|
||||
:py:class:`Object <volatility.framework.interfaces.objects.ObjectInterface>` has been created, the data has been read from the
|
||||
Using a :py:class:`~volatility3.framework.interfaces.objects.Template` on a memory layer at a particular offset, an
|
||||
:py:class:`Object <volatility3.framework.interfaces.objects.ObjectInterface>` can be constructed. In Volatility 3, once an
|
||||
:py:class:`Object <volatility3.framework.interfaces.objects.ObjectInterface>` has been created, the data has been read from the
|
||||
layer and is not read again. An object allows its members to be interrogated and in particular allows pointers to be
|
||||
followed, providing easy access to the data contained in the object.
|
||||
|
||||
@@ -62,23 +62,23 @@ Symbol Tables
|
||||
-------------
|
||||
|
||||
Most compiled programs know of their own templates, and define the structure (and location within the program) of these
|
||||
templates as a :py:class:`Symbol <volatility.framework.interfaces.symbols.SymbolInterface>`. A
|
||||
:py:class:`Symbol <volatility.framework.interfaces.symbols.SymbolInterface>` is often an address and a template and can
|
||||
templates as a :py:class:`Symbol <volatility3.framework.interfaces.symbols.SymbolInterface>`. A
|
||||
:py:class:`Symbol <volatility3.framework.interfaces.symbols.SymbolInterface>` is often an address and a template and can
|
||||
be used to refer to either independently. Lookup tables of these symbols are often produced as debugging information
|
||||
alongside the compilation of the program. Volatility 3 provides access to these through a
|
||||
:py:class:`SymbolTable <volatility.framework.interfaces.symbols.SymbolTableInterface>`, many of which can be collected
|
||||
within a :py:class:`~volatility.framework.contexts.Context` as a :py:class:`SymbolSpace <volatility.framework.interfaces.symbols.SymbolSpaceInterface>`.
|
||||
A :py:class:`~volatility.framework.contexts.Context` can store only one :py:class:`~volatility.framework.symbols.SymbolSpace`
|
||||
at a time, although a :py:class:`~volatility.framework.symbols.SymbolSpace` can store as
|
||||
many :py:class:`~volatility.framework.symbols.SymbolTable` items as necessary.
|
||||
:py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>`, many of which can be collected
|
||||
within a :py:class:`~volatility3.framework.contexts.Context` as a :py:class:`SymbolSpace <volatility.framework.interfaces.symbols.SymbolSpaceInterface>`.
|
||||
A :py:class:`~volatility3.framework.contexts.Context` can store only one :py:class:`~volatility.framework.symbols.SymbolSpace`
|
||||
at a time, although a :py:class:`~volatility3.framework.symbols.SymbolSpace` can store as
|
||||
many :py:class:`~volatility3.framework.symbols.SymbolTable` items as necessary.
|
||||
|
||||
Volatility 3 uses the de facto naming convention for symbols of `module!symbol` to refer to them. It reads them from its
|
||||
own JSON formatted file, which acts as a common intermediary between Windows PDB files, Linux DWARF files, other symbol
|
||||
formats and the internal Python format that Volatility 3 uses to represent
|
||||
a :py:class:`~volatility.framework.interfaces.objects.Template` or
|
||||
a :py:class:`Symbol <volatility.framework.interfaces.symbols.SymbolInterface>`.
|
||||
a :py:class:`~volatility3.framework.interfaces.objects.Template` or
|
||||
a :py:class:`Symbol <volatility3.framework.interfaces.symbols.SymbolInterface>`.
|
||||
|
||||
.. note:: Volatility 2's name for a :py:class:`~volatility.framework.symbols.SymbolSpace` was a profile, but it could
|
||||
.. note:: Volatility 2's name for a :py:class:`~volatility3.framework.symbols.SymbolSpace` was a profile, but it could
|
||||
not differentiate between symbols from different modules and required special handling for 32-bit programs that
|
||||
used Wow64 on Windows. This meant that all symbols lived in a single namespace with the possibility of symbol name
|
||||
collisions. It read the symbols using a format called *vtypes*, written in Python code directly.
|
||||
@@ -88,18 +88,18 @@ Plugins
|
||||
-------
|
||||
|
||||
A plugin acts as a means of requesting data from the user interface (and so the user) and then using it to carry out a
|
||||
specific form of analysis on the :py:class:`Context <volatility.framework.interfaces.context.ContextInterface>`
|
||||
specific form of analysis on the :py:class:`Context <volatility3.framework.interfaces.context.ContextInterface>`
|
||||
(containing whatever symbol tables and memory layers it may). The means of communication between the user interface and
|
||||
the library is the configuration tree, which is used by components within the :py:class:`~volatility.framework.contexts.Context`
|
||||
the library is the configuration tree, which is used by components within the :py:class:`~volatility3.framework.contexts.Context`
|
||||
to store configurable data. After the plugin has been run, it then returns the results in a specific format known as a
|
||||
:py:class:`~volatility.framework.interfaces.renderers.TreeGrid`. This ensures that the data can be handled by consumers of
|
||||
:py:class:`~volatility3.framework.interfaces.renderers.TreeGrid`. This ensures that the data can be handled by consumers of
|
||||
the library, without knowing exactly what the data is or how it's formatted.
|
||||
|
||||
Output Renderers
|
||||
----------------
|
||||
|
||||
User interfaces can choose how best to present the output of the results to their users. The library always responds from
|
||||
every plugin with a :py:class:`~volatility.framework.renderers.TreeGrid`, and the user interface can then determine how
|
||||
every plugin with a :py:class:`~volatility3.framework.renderers.TreeGrid`, and the user interface can then determine how
|
||||
best to display it. For the Command Line Interface, that might be via text output as a table, or it might output to an
|
||||
SQLite database or a CSV file. For a web interface, the best output is probably as JSON where it could be displayed as
|
||||
a table, or inserted into a database like Elastic Search and trawled using an existing frontend such as Kibana.
|
||||
@@ -111,9 +111,9 @@ Configuration Tree
|
||||
------------------
|
||||
|
||||
The configuration tree acts as the interface between the calling program and Volatility 3 library. Elements of the
|
||||
library (such as a :py:class:`Plugin <volatility.framework.interfaces.plugins.PluginInterface>`,
|
||||
a :py:class:`TranslationLayer <volatility.framework.interfaces.layers.TranslationLayerInterface>`,
|
||||
an :py:class:`Automagic <volatility.framework.interfaces.automagic.AutomagicInterface>`, etc.) can use the configuration
|
||||
library (such as a :py:class:`Plugin <volatility3.framework.interfaces.plugins.PluginInterface>`,
|
||||
a :py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>`,
|
||||
an :py:class:`Automagic <volatility3.framework.interfaces.automagic.AutomagicInterface>`, etc.) can use the configuration
|
||||
tree to inform the calling program of the options they require and/or optionally support, and allows the calling program
|
||||
to provide that information when the library is then called.
|
||||
|
||||
@@ -122,7 +122,7 @@ Automagic
|
||||
|
||||
There are certain setup tasks that establish the context in a way favorable to a plugin before it runs, removing
|
||||
several tasks that are repetitive and also easy to get wrong. These are called
|
||||
:py:class:`Automagic <volatility.framework.interfaces.automagic.AutomagicInterface>`, since they do things like magically
|
||||
:py:class:`Automagic <volatility3.framework.interfaces.automagic.AutomagicInterface>`, since they do things like magically
|
||||
taking a raw memory image and automatically providing the plugin with an appropriate Intel translation layer and an
|
||||
accurate symbol table without either the plugin or the calling program having to specify all the necessary details.
|
||||
|
||||
|
||||
@@ -6,17 +6,17 @@ which are discussed below.
|
||||
|
||||
Writing Reusable Methods
|
||||
------------------------
|
||||
Classes which inherit from :py:class:`~volatility.framework.interfaces.plugins.PluginInterface` all have a :py:meth:`run()` method
|
||||
which takes no parameters and will return a :py:class:`~volatility.framework.interfaces.renderers.TreeGrid`. Since most useful
|
||||
Classes which inherit from :py:class:`~volatility3.framework.interfaces.plugins.PluginInterface` all have a :py:meth:`run()` method
|
||||
which takes no parameters and will return a :py:class:`~volatility3.framework.interfaces.renderers.TreeGrid`. Since most useful
|
||||
functions are parameterized, to provide parameters to a plugin the `configuration` for the context must be appropriately manipulated.
|
||||
There is scope for this, in order to run multiple plugins (see `Writing plugins that run other plugins`) but a much simpler method
|
||||
is to provide a parameterized `classmethod` within the plugin, which will allow the method to yield whatever kind of output it will
|
||||
generate and take whatever parameters it might need.
|
||||
|
||||
This is how processes are listed, which is an often used function. The code lives within the
|
||||
:py:class:`~volatility.plugins.windows.pslist.PsList` plugin but can be used by other plugins by providing the
|
||||
:py:class:`~volatility3.plugins.windows.pslist.PsList` plugin but can be used by other plugins by providing the
|
||||
appropriate parameters (see
|
||||
:py:meth:`~volatility.plugins.windows.pslist.PsList.list_processes`).
|
||||
:py:meth:`~volatility3.plugins.windows.pslist.PsList.list_processes`).
|
||||
It is up to the author of a plugin to validate that any required plugins are present and are the appropriate version.
|
||||
|
||||
Writing plugins that run other plugins
|
||||
@@ -34,7 +34,7 @@ available plugins that feature a Timeliner interface). This can be achieved wit
|
||||
This code will first generate suitable automagics for running against the context. Unfortunately this must be re-run for
|
||||
each plugin in order to populate the context's configuration correctly based on the plugin's requirements (which may vary
|
||||
between plugins). Once the automagics have been constructed, the plugin can be instantiated using the helper function
|
||||
:py:func:`~volatility.framework.plugins.construct_plugin` providing:
|
||||
:py:func:`~volatility3.framework.plugins.construct_plugin` providing:
|
||||
|
||||
* the base context (containing the configuration and any already loaded layers or symbol tables),
|
||||
* the plugin class to run,
|
||||
@@ -43,7 +43,7 @@ between plugins). Once the automagics have been constructed, the plugin can be
|
||||
* an open method for the plugin to create files during the run
|
||||
|
||||
With the constructed plugin, it can either be run by calling its
|
||||
:py:meth:`~volatility.framework.interfaces.plugins.PluginInterface.run` method, or any other known method can
|
||||
:py:meth:`~volatility3.framework.interfaces.plugins.PluginInterface.run` method, or any other known method can
|
||||
be invoked on it.
|
||||
|
||||
Writing plugins that output files
|
||||
@@ -55,7 +55,7 @@ an abstraction layer is used.
|
||||
The user interface specifies an open_method (which is actually a class constructor that can double as a python
|
||||
ContextManager, so it can be used by the python `with` keyword). This is set on the plugin using
|
||||
`plugin.set_open_method` and can then be called or accessed using `plugin.open(preferred_filename)`. There are no additional options
|
||||
that can be set on the filename, and a :py:class:`~volatility.framework.interfaces.plugins.FileHandlerInterface` is the result.
|
||||
that can be set on the filename, and a :py:class:`~volatility3.framework.interfaces.plugins.FileHandlerInterface` is the result.
|
||||
This mimics an `IO[bytes]` object, which closely mimics a standard python file-like object.
|
||||
|
||||
As such code for outputting to a file would be expected to look something like:
|
||||
@@ -73,10 +73,10 @@ closed to allow the preferred filename to be changed (or data to be added/modifi
|
||||
Writing Scanners
|
||||
----------------
|
||||
|
||||
Scanners are objects that adhere to the :py:class:`~volatility.framework.interfaces.layers.ScannerInterface`. They are
|
||||
passed to the :py:meth:`~volatility.framework.interfaces.layers.TranslationLayerInterface.scan` method on layers which will
|
||||
Scanners are objects that adhere to the :py:class:`~volatility3.framework.interfaces.layers.ScannerInterface`. They are
|
||||
passed to the :py:meth:`~volatility3.framework.interfaces.layers.TranslationLayerInterface.scan` method on layers which will
|
||||
divide the provided range of sections (or the entire layer
|
||||
if none are provided) and call the :py:meth:`~volatility.framework.interfaces.layers.ScannerInterface`'s call method
|
||||
if none are provided) and call the :py:meth:`~volatility3.framework.interfaces.layers.ScannerInterface`'s call method
|
||||
method with each chunk as a parameter, ensuring a suitable amount of overlap (as defined by the scanner).
|
||||
The offset of the chunk, within the layer, is also provided as a parameter.
|
||||
|
||||
@@ -96,7 +96,7 @@ Writing/Using Intermediate Symbol Format Files
|
||||
----------------------------------------------
|
||||
|
||||
It can occasionally be useful to create a data file containing the static structures that can create a
|
||||
:py:class:`~volatility.framework.interfaces.objects.Template` to be instantiated on a layer.
|
||||
:py:class:`~volatility3.framework.interfaces.objects.Template` to be instantiated on a layer.
|
||||
Volatility has all the machinery necessary to construct these for you from properly formatted JSON data.
|
||||
|
||||
The JSON format is documented by the JSON schema files located in schemas. These are versioned using standard .so
|
||||
@@ -136,7 +136,7 @@ Another useful parameter is `table_mapping` which allows for type referenced ins
|
||||
table_mapping = {'one_table': 'another_table'})
|
||||
|
||||
The last parameter that can be used is called `class_types` which allows a particular structure to be instantiated on
|
||||
a class other than :py:class:`~volatility.framework.objects.StructType`, allowing for additional methods to be defined
|
||||
a class other than :py:class:`~volatility3.framework.objects.StructType`, allowing for additional methods to be defined
|
||||
and associated with the type.
|
||||
|
||||
The table name can then by used to access the constructed table from the context, such as:
|
||||
@@ -152,7 +152,7 @@ Translation layers offer a way for data to be translated from a higher (domain)
|
||||
The main method that must be overloaded for a translation layer is the `mapping` method. Usually this is a linear
|
||||
mapping whereby a value at an offset in the domain maps directly to an offset in the range.
|
||||
|
||||
Most new layers should inherit from :py:class:`~volatility.framework.layers.linear.LinearlyMappedLayer` where they
|
||||
Most new layers should inherit from :py:class:`~volatility3.framework.layers.linear.LinearlyMappedLayer` where they
|
||||
can define a mapping method as follows:
|
||||
|
||||
.. code-block:: python
|
||||
@@ -205,7 +205,7 @@ This mechanism also allowed for some minor optimization in scanning such a layer
|
||||
scanning of layers be needed, please refer to the Layer Scanning page.
|
||||
|
||||
Whilst it may seem as though some of the data seems redundant (the length values are always the same) this is not the
|
||||
case for :py:class:`~volatility.framework.layers.segmented.NonLinearlySegmentedLayer`. These layers do not guarantee
|
||||
case for :py:class:`~volatility3.framework.layers.segmented.NonLinearlySegmentedLayer`. These layers do not guarantee
|
||||
that each domain address maps directly to a range address, and in fact can carry out processing on the data. These
|
||||
layers are most commonly encountered as compression or encryption layers (whereby a domain address may map into a
|
||||
chunk of the range, but not directly). In this instance, the mapping will likely define additional methods that can
|
||||
@@ -285,8 +285,8 @@ Writing new Templates and Objects
|
||||
---------------------------------
|
||||
|
||||
In most cases, a whole new type of object is unnecessary. It will usually be derived from an
|
||||
:py:class:`~volatility.framework.objects.StructType` (which is itself just another name for a
|
||||
:py:class:`~volatility.framework.objects.AggregateType`, but it's better to use `StructType` for readability).
|
||||
:py:class:`~volatility3.framework.objects.StructType` (which is itself just another name for a
|
||||
:py:class:`~volatility3.framework.objects.AggregateType`, but it's better to use `StructType` for readability).
|
||||
|
||||
This can be used as a class override for a particular symbol table, so that an existing structure can be augmented with
|
||||
additional methods. An example of this would be:
|
||||
@@ -300,27 +300,27 @@ This will mean that when a specific structure is loaded from the symbol_space, i
|
||||
`StructType`, but instead is instantiated using the NewStructureClass, meaning new methods can be called directly on it.
|
||||
|
||||
If the situation really calls for an entirely new object, that isn't covered by one of the existing
|
||||
:py:class:`~volatility.framework.objects.PrimativeObject` objects (such as
|
||||
:py:class:`~volatility.framework.objects.Integer`,
|
||||
:py:class:`~volatility.framework.objects.Boolean`,
|
||||
:py:class:`~volatility.framework.objects.Float`,
|
||||
:py:class:`~volatility.framework.objects.Char`,
|
||||
:py:class:`~volatility.framework.objects.Bytes`)
|
||||
:py:class:`~volatility3.framework.objects.PrimativeObject` objects (such as
|
||||
:py:class:`~volatility3.framework.objects.Integer`,
|
||||
:py:class:`~volatility3.framework.objects.Boolean`,
|
||||
:py:class:`~volatility3.framework.objects.Float`,
|
||||
:py:class:`~volatility3.framework.objects.Char`,
|
||||
:py:class:`~volatility3.framework.objects.Bytes`)
|
||||
or the other builtins (such as
|
||||
:py:class:`~volatility.framework.objects.Array`,
|
||||
:py:class:`~volatility.framework.objects.Bitfield`,
|
||||
:py:class:`~volatility.framework.objects.Enumeration`,
|
||||
:py:class:`~volatility.framework.objects.Pointer`,
|
||||
:py:class:`~volatility.framework.objects.String`,
|
||||
:py:class:`~volatility.framework.objects.Void`) then you can review the following information about defining an entirely
|
||||
:py:class:`~volatility3.framework.objects.Array`,
|
||||
:py:class:`~volatility3.framework.objects.Bitfield`,
|
||||
:py:class:`~volatility3.framework.objects.Enumeration`,
|
||||
:py:class:`~volatility3.framework.objects.Pointer`,
|
||||
:py:class:`~volatility3.framework.objects.String`,
|
||||
:py:class:`~volatility3.framework.objects.Void`) then you can review the following information about defining an entirely
|
||||
new object.
|
||||
|
||||
All objects must inherit from :py:class:`~volatility.framework.interfaces.objects.ObjectInterface` which defines a
|
||||
constructor that takes a context, a `type_name`, an :py:class:`~volatility.framework.interfaces.objects.ObjectInformation`
|
||||
All objects must inherit from :py:class:`~volatility3.framework.interfaces.objects.ObjectInterface` which defines a
|
||||
constructor that takes a context, a `type_name`, an :py:class:`~volatility3.framework.interfaces.objects.ObjectInformation`
|
||||
object and then can accept additional keywords (which will not necessarily be provided if the object is constructed
|
||||
from a JSON reference).
|
||||
|
||||
The :py:class:`~volatility.framework.interfaces.objects.ObjectInformation` class contains all the basic elements that
|
||||
The :py:class:`~volatility3.framework.interfaces.objects.ObjectInformation` class contains all the basic elements that
|
||||
define an object, which include:
|
||||
|
||||
* layer_name
|
||||
@@ -345,10 +345,10 @@ should be. Note, the size can change throughout the lifespan of the object, and
|
||||
it compensates for such a change.
|
||||
|
||||
Objects must also contain a specific class called `VolTemplateProxy` which must inherit from
|
||||
:py:class:`~volatility.framework.interfaces.objects.ObjectInterface`. This is used to access information about
|
||||
:py:class:`~volatility3.framework.interfaces.objects.ObjectInterface`. This is used to access information about
|
||||
a structure before it has been associated with data and becomes an Object. The
|
||||
:py:class:`~volatility.framework.interfaces.objects.ObjectInterface.VolTemplateProxy` class contains a number of
|
||||
abstract classmethods, which take a :py:class:`~volatility.framework.interfaces.objects.Template`. The main method
|
||||
:py:class:`~volatility3.framework.interfaces.objects.ObjectInterface.VolTemplateProxy` class contains a number of
|
||||
abstract classmethods, which take a :py:class:`~volatility3.framework.interfaces.objects.Template`. The main method
|
||||
that is likely to need overwriting is the `size` method, which should return the size of the object (for the template
|
||||
of a dynamically-sized object, this should be a suitable value, and calculated based on the best available information).
|
||||
For most objects, this can be determined from the JSON data used to construct a normal `Struct` and therefore only needs
|
||||
|
||||
@@ -3,19 +3,19 @@ How to Write a Simple Plugin
|
||||
|
||||
This guide will step through how to construct a simple plugin using Volatility 3.
|
||||
|
||||
The example plugin we'll use is :py:class:`~volatility.plugins.windows.dlllist.DllList`, which features the main traits
|
||||
The example plugin we'll use is :py:class:`~volatility3.plugins.windows.dlllist.DllList`, which features the main traits
|
||||
of a normal plugin, and reuses other plugins appropriately.
|
||||
|
||||
Inherit from PluginInterface
|
||||
----------------------------
|
||||
|
||||
The first step is to define a class that inherits from :py:class:`~volatility.framework.interfaces.plugins.PluginInterface`.
|
||||
The first step is to define a class that inherits from :py:class:`~volatility3.framework.interfaces.plugins.PluginInterface`.
|
||||
Volatility automatically finds all plugins defined under the various plugin directories by importing them and then
|
||||
making use of any classes that inherit from :py:class:`~volatility.framework.interfaces.plugins.PluginInterface`.
|
||||
making use of any classes that inherit from :py:class:`~volatility3.framework.interfaces.plugins.PluginInterface`.
|
||||
|
||||
::
|
||||
|
||||
from volatility.framework import interfaces
|
||||
from volatility3.framework import interfaces
|
||||
|
||||
class DllList(interfaces.plugins.PluginInterface):
|
||||
|
||||
@@ -56,7 +56,7 @@ to instantiate the plugin). At the moment these requirements are fairly straigh
|
||||
architectures = ["Intel32", "Intel64"]),
|
||||
|
||||
This requirement indicates that the plugin will operate on a single
|
||||
:py:class:`TranslationLayer <volatility.framework.interfaces.layers.TranslationLayerInterface>`. The name of the
|
||||
:py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>`. The name of the
|
||||
loaded layer will appear in the plugin's configuration under the name ``primary``. Requirement values can be
|
||||
accessed within the plugin through the plugin's `config` attribute (for example ``self.config['pid']``).
|
||||
|
||||
@@ -71,7 +71,7 @@ layers, for example a plugin that carries out some form of difference or statist
|
||||
|
||||
This requirement (and the next two) are known as Complex Requirements, and user interfaces will likely not directly
|
||||
request a value for this from a user. The value stored in the configuration tree for a
|
||||
:py:class:`~volatility.framework.configuration.requirements.TranslationLayerRequirement` is
|
||||
:py:class:`~volatility3.framework.configuration.requirements.TranslationLayerRequirement` is
|
||||
the string name of a layer present in the context's memory that satisfies the requirement.
|
||||
|
||||
::
|
||||
@@ -80,14 +80,14 @@ the string name of a layer present in the context's memory that satisfies the re
|
||||
description = "Windows kernel symbols"),
|
||||
|
||||
This requirement specifies the need for a particular
|
||||
:py:class:`SymbolTable <volatility.framework.interfaces.symbols.SymbolTableInterface>`
|
||||
:py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>`
|
||||
to be loaded. This gets populated by various
|
||||
:py:class:`Automagic <volatility.framework.interfaces.automagic.AutoMagicInterface>` as the nearest sibling to a particular
|
||||
:py:class:`~volatility.framework.configuration.requirements.TranslationLayerRequirement`.
|
||||
This means that if the :py:class:`~volatility.framework.configuration.requirements.TranslationLayerRequirement`
|
||||
is satisfied and the :py:class:`Automagic <volatility.framework.interfaces.automagic.AutoMagicInterface>` can determine
|
||||
the appropriate :py:class:`SymbolTable <volatility.framework.interfaces.symbols.SymbolTableInterface>`, the
|
||||
name of the :py:class:`SymbolTable <volatility.framework.interfaces.symbols.SymbolTableInterface>` will be stored in the configuration.
|
||||
:py:class:`Automagic <volatility3.framework.interfaces.automagic.AutoMagicInterface>` as the nearest sibling to a particular
|
||||
:py:class:`~volatility3.framework.configuration.requirements.TranslationLayerRequirement`.
|
||||
This means that if the :py:class:`~volatility3.framework.configuration.requirements.TranslationLayerRequirement`
|
||||
is satisfied and the :py:class:`Automagic <volatility3.framework.interfaces.automagic.AutoMagicInterface>` can determine
|
||||
the appropriate :py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>`, the
|
||||
name of the :py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>` will be stored in the configuration.
|
||||
|
||||
This requirement is also a Complex Requirement and therefore will not be requested directly from the user.
|
||||
|
||||
@@ -119,10 +119,10 @@ Define the `run` method
|
||||
|
||||
The run method is the primary method called on a plugin. It takes no parameters (these have been passed through the
|
||||
context's configuration tree, and the context is provided at plugin initialization time) and returns an unpopulated
|
||||
:py:class:`~volatility.framework.interfaces.renderers.TreeGrid` object. These are typically constructed based on a
|
||||
:py:class:`~volatility3.framework.interfaces.renderers.TreeGrid` object. These are typically constructed based on a
|
||||
generator that carries out the bulk of the plugin's processing. The
|
||||
:py:class:`~volatility.framework.interfaces.renderers.TreeGrid` also specifies the column names and types
|
||||
that will be output as part of the :py:class:`~volatility.framework.interfaces.renderers.TreeGrid`.
|
||||
:py:class:`~volatility3.framework.interfaces.renderers.TreeGrid` also specifies the column names and types
|
||||
that will be output as part of the :py:class:`~volatility3.framework.interfaces.renderers.TreeGrid`.
|
||||
|
||||
::
|
||||
|
||||
@@ -143,28 +143,28 @@ that will be output as part of the :py:class:`~volatility.framework.interfaces.r
|
||||
|
||||
In this instance, the plugin constructs a filter (using the PsList plugin's *classmethod* for creating filters).
|
||||
It checks the plugin's configuration for the ``pid`` value, and passes it in as a list if it finds it, or None if
|
||||
it does not. The :py:func:`~volatility.plugins.windows.pslist.PsList.create_pid_filter` method accepts a list of process
|
||||
it does not. The :py:func:`~volatility3.plugins.windows.pslist.PsList.create_pid_filter` method accepts a list of process
|
||||
identifiers that are included in the list. If the list is empty, all processes are returned.
|
||||
|
||||
The next line specifies the columns by their name and type. The types are simple types (int, str, bytes, float, and bool)
|
||||
but can also provide hints as to how the output should be displayed (such as a hexidecimal number, using
|
||||
:py:class:`volatility.framework.renderers.format_hints.Hex`).
|
||||
:py:class:`volatility3.framework.renderers.format_hints.Hex`).
|
||||
This indicates to user interfaces that the value should be displayed in a particular way, but does not guarantee that the value
|
||||
will be displayed that way (for example, if it doesn't make sense to do so in a particular interface).
|
||||
|
||||
Finally, the generator is provided. The generator accepts a list of processes, which is gathered using a different plugin,
|
||||
the :py:class:`~volatility.plugins.windows.pslist.PsList` plugin. That plugin features a *classmethod*,
|
||||
the :py:class:`~volatility3.plugins.windows.pslist.PsList` plugin. That plugin features a *classmethod*,
|
||||
so that other plugins can call it. As such, it takes all the necessary parameters rather than accessing them
|
||||
from a configuration. Since it must be portable code, it takes a context, as well as the layer name,
|
||||
symbol table and optionally a filter. In this instance we unconditionally
|
||||
pass it the values from the configuration for the ``primary`` and ``nt_symbols`` requirements. This will generate a list
|
||||
of :py:class:`~volatility.framework.symbols.windows.extensions.EPROCESS` objects, as provided by the :py:class:`~volatility.plugins.windows.pslist.PsList` plugin,
|
||||
of :py:class:`~volatility3.framework.symbols.windows.extensions.EPROCESS` objects, as provided by the :py:class:`~volatility.plugins.windows.pslist.PsList` plugin,
|
||||
and is not covered here but is used as an example for how to share code across plugins
|
||||
(both as the provider and the consumer of the shared code).
|
||||
|
||||
Define the generator
|
||||
--------------------
|
||||
The :py:class:`~volatility.framework.interfaces.renderers.TreeGrid` can be populated without a generator,
|
||||
The :py:class:`~volatility3.framework.interfaces.renderers.TreeGrid` can be populated without a generator,
|
||||
but it is quite a common model to use. This is where the main processing for this plugin lives.
|
||||
|
||||
::
|
||||
@@ -189,10 +189,10 @@ but it is quite a common model to use. This is where the main processing for th
|
||||
format_hints.Hex(entry.DllBase), format_hints.Hex(entry.SizeOfImage),
|
||||
BaseDllName, FullDllName))
|
||||
|
||||
This iterates through the list of processes and for each one calls the :py:meth:`~volatility.framework.symbols.windows.extensions.EPROCESS.load_order_modules` method on it. This provides
|
||||
This iterates through the list of processes and for each one calls the :py:meth:`~volatility3.framework.symbols.windows.extensions.EPROCESS.load_order_modules` method on it. This provides
|
||||
a list of the loaded modules within the process.
|
||||
|
||||
The plugin then defaults the ``BaseDllName`` and ``FullDllName`` variables to an :py:class:`~volatility.framework.renderers.UnreadableValue`,
|
||||
The plugin then defaults the ``BaseDllName`` and ``FullDllName`` variables to an :py:class:`~volatility3.framework.renderers.UnreadableValue`,
|
||||
which is a way of indicating to the user interface that the value couldn't be read for some reason (but that it isn't fatal).
|
||||
There are currently four different reasons a value may be unreadable:
|
||||
|
||||
@@ -204,7 +204,7 @@ There are currently four different reasons a value may be unreadable:
|
||||
This is a safety provision to ensure that the data returned by the Volatility library is accurate and describes why
|
||||
information may not be provided.
|
||||
|
||||
The plugin then takes the process's ``BaseDllName`` value, and calls :py:meth:`~volatility.framework.symbols.windows.extensions.UNICODE_STRING.get_string` on it. All structure attributes,
|
||||
The plugin then takes the process's ``BaseDllName`` value, and calls :py:meth:`~volatility3.framework.symbols.windows.extensions.UNICODE_STRING.get_string` on it. All structure attributes,
|
||||
as defined by the symbols, are directly accessible and use the case-style of the symbol library it came from (in Windows,
|
||||
attributes are CamelCase), such as ``entry.BaseDllName`` in this instance. Any attribtues not defined by the symbol but added
|
||||
by Volatility extensions cannot be properties (in case they overlap with the attributes defined in the symbol libraries)
|
||||
@@ -215,16 +215,16 @@ read the data at a particular offset. This will cause an exception to be thrown
|
||||
as a means of communicating when something exceptional happens. It is the responsibility of the plugin developer to
|
||||
appropriately catch and handle any non-fatal exceptions and otherwise allow the exception to be thrown by the user interface.
|
||||
|
||||
In this instance, the :py:class:`~volatility.framework.exceptions.InvalidAddressException` class is caught, which is thrown
|
||||
In this instance, the :py:class:`~volatility3.framework.exceptions.InvalidAddressException` class is caught, which is thrown
|
||||
by any layer which cannot access an offset requested of it. Since we have already populated both values with ``UnreadableValue``
|
||||
we do not need to write code for the exception handler.
|
||||
|
||||
Finally, we yield the record in the format required by the :py:class:`~volatility.framework.interfaces.renderers.TreeGrid`,
|
||||
Finally, we yield the record in the format required by the :py:class:`~volatility3.framework.interfaces.renderers.TreeGrid`,
|
||||
a tuple, listing the indentation level (for trees) and then the list of values for each column.
|
||||
This plugin demonstrates casting a value ``ImageFileName`` to ensure it's returned
|
||||
as a string with a specific maximum length, rather than its original type (potentially an array of characters, etc).
|
||||
This is carried out using the :py:meth:`~volatility.framework.interfaces.objects.ObjectInterface.cast` method which takes a type (either a native type, such as string or pointer, or a
|
||||
structure type defined in a :py:class:`SymbolTable <volatility.framework.interfaces.symbols.SymbolTableInterface>`
|
||||
This is carried out using the :py:meth:`~volatility3.framework.interfaces.objects.ObjectInterface.cast` method which takes a type (either a native type, such as string or pointer, or a
|
||||
structure type defined in a :py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>`
|
||||
such as ``<table>!_UNICODE``) and the parameters to that type.
|
||||
|
||||
Since the cast value must populate a string typed column, it had to be a Python string (such as being cast to the native
|
||||
|
||||
@@ -25,7 +25,7 @@ from versions 1.1 or 1.2:
|
||||
|
||||
::
|
||||
|
||||
volatility.framework.require_interface_version(1, 0, 0)
|
||||
volatility3.framework.require_interface_version(1, 0, 0)
|
||||
|
||||
Contexts can be spun up quite easily, just construct one. It's not a singleton, so multiple contexts can be
|
||||
constructed and operate independently, but be aware of which context you're handing where and make sure to use
|
||||
@@ -42,20 +42,20 @@ Determine what plugins are available
|
||||
------------------------------------
|
||||
|
||||
You can also interrogate the framework to see which plugins are available. First we have to try to load all
|
||||
available plugins. The :py:func:`~volatility.framework.import_files` method will automatically use the module
|
||||
paths for the provided module (in this case, volatility.plugins) and walk the directory (or directories) loading up
|
||||
available plugins. The :py:func:`~volatility3.framework.import_files` method will automatically use the module
|
||||
paths for the provided module (in this case, volatility3.plugins) and walk the directory (or directories) loading up
|
||||
all python files. Any import failures will be provided in the failures return value, unless the second parameter is
|
||||
False in which case the call will raise any exceptions encountered. Any additional directories containing plugins
|
||||
should be added to the `__path__` attribute for the `volatility.plugins` module. The standard paths should generally
|
||||
also be included, which can be found in `volatility.constants.PLUGINS_PATH`.
|
||||
should be added to the `__path__` attribute for the `volatility3.plugins` module. The standard paths should generally
|
||||
also be included, which can be found in `volatility3.constants.PLUGINS_PATH`.
|
||||
|
||||
::
|
||||
|
||||
volatility.plugins.__path__ = <new_plugin_path> + constants.PLUGINS_PATH
|
||||
failures = framework.import_files(volatility.plugins, True)
|
||||
volatility3.plugins.__path__ = <new_plugin_path> + constants.PLUGINS_PATH
|
||||
failures = framework.import_files(volatility3.plugins, True)
|
||||
|
||||
Once the plugins have been imported, we can interrogate which plugins are available. The
|
||||
:py:func:`~volatility.framework.list_plugins` call will
|
||||
:py:func:`~volatility3.framework.list_plugins` call will
|
||||
return a dictionary of plugin names and the plugin classes.
|
||||
|
||||
::
|
||||
@@ -68,9 +68,9 @@ Determine what configuration options a plugin requires
|
||||
------------------------------------------------------
|
||||
|
||||
For each plugin class, we can call the classmethod `requirements` on it, which will return a list of objects that
|
||||
adhere to the :py:class:`~volatility.framework.interfaces.configuration.RequirementInterface` method. The various
|
||||
adhere to the :py:class:`~volatility3.framework.interfaces.configuration.RequirementInterface` method. The various
|
||||
types of Requirement are split roughly in two,
|
||||
:py:class:`~volatility.framework.interfaces.configuration.SimpleTypeRequirement` (such as integers, booleans, floats
|
||||
:py:class:`~volatility3.framework.interfaces.configuration.SimpleTypeRequirement` (such as integers, booleans, floats
|
||||
and strings) and more complex requirements (such as lists, choices, multiple requirements, translation layer
|
||||
requirements or symbol table requirements). A requirement just specifies a type of data and a name, and must be
|
||||
combined with a configuration hierarchy to have meaning.
|
||||
@@ -98,7 +98,7 @@ underneaths its own branch). To set the hierarchy, you'll need to know where th
|
||||
For this example, we'll assume plugins' base_config_path is set as `plugins`, and that automagics are configured under
|
||||
the `automagic` tree. We'll see later how to ensure this matches up with the plugins and automagic when they're
|
||||
constructed. Joining configuration options should always be carried out using
|
||||
:py:func:`~volatility.framework.interfaces.configuration.path_join`
|
||||
:py:func:`~volatility3.framework.interfaces.configuration.path_join`
|
||||
in case the separator value gets changed in the future. Configuration items can then be set as follows:
|
||||
|
||||
::
|
||||
@@ -170,7 +170,7 @@ be called whenever a plugin produces an auxiliary file.
|
||||
constructed = plugin(context, plugin_config_path, progress_callback = progress_callback)
|
||||
constructed.set_open_method(file_handler)
|
||||
|
||||
The file_handler must adhere to the :py:class:`~volatility.framework.interfaces.plugins.FileHandlerInterface`,
|
||||
The file_handler must adhere to the :py:class:`~volatility3.framework.interfaces.plugins.FileHandlerInterface`,
|
||||
which represents an IO[bytes] object but also contains a `preferred_filename` attribute as a hint.
|
||||
|
||||
All of this functionality has been condensed into a framework method called `construct_plugin` which will
|
||||
@@ -181,7 +181,7 @@ accepts an optional progress_callback and an optional file_consumer.
|
||||
|
||||
constructed = plugins.construct_plugin(ctx, automagics, plugin, base_config_path, progress_callback, file_consumer)
|
||||
|
||||
Finally the plugin can be run, and will return a :py:class:`~volatility.framework.interfaces.renderers.TreeGrid`.
|
||||
Finally the plugin can be run, and will return a :py:class:`~volatility3.framework.interfaces.renderers.TreeGrid`.
|
||||
|
||||
::
|
||||
|
||||
@@ -201,22 +201,22 @@ does the actual work. This can return an exception if one occurs during the run
|
||||
|
||||
The results can be accessed either as the results are being processed, or by visiting the nodes in the tree
|
||||
once it is fully populated. In either case, a visitor method will be required. The visitor method
|
||||
should accept a :py:class:`~volatility.framework.interfaces.renderers.TreeNode` and an `accumulator`. It will
|
||||
should accept a :py:class:`~volatility3.framework.interfaces.renderers.TreeNode` and an `accumulator`. It will
|
||||
return an updated accumulator.
|
||||
|
||||
When provided a :py:class:`~volatility.framework.interfaces.renderers.TreeNode`, it can be accessed as a dictionary
|
||||
When provided a :py:class:`~volatility3.framework.interfaces.renderers.TreeNode`, it can be accessed as a dictionary
|
||||
based on the column names that the treegrid contains. It should be noted that each column can contain only the
|
||||
type specified in the `column.type` field (which can be a simple type like string, integer, float, bytes or
|
||||
a more complex type, like a DateTime, a Disassembly or a descendant of
|
||||
:py:class:`~volatility.framework.interfaces.renderers.BaseAbsentValue`). The various fields may also be wrapped in
|
||||
:py:class:`~volatility3.framework.interfaces.renderers.BaseAbsentValue`). The various fields may also be wrapped in
|
||||
`format_hints` designed to tell the user interface how to render the data. These hints can be things like Bin, Hex or
|
||||
HexBytes, so that fields like offsets are displayed in hex form or so that bytes are displayed in their hex form rather
|
||||
than their raw form. Descendants of :py:class:`~volatility.framework.interfaces.renderers.BaseAbsentValue` can currently
|
||||
than their raw form. Descendants of :py:class:`~volatility3.framework.interfaces.renderers.BaseAbsentValue` can currently
|
||||
be one of
|
||||
:py:class:`~volatility.framework.renderers.UnreadableValue`,
|
||||
:py:class:`~volatility.framework.renderers.UnparsableValue`,
|
||||
:py:class:`~volatility.framework.renderers.NotApplicableValue` or
|
||||
:py:class:`~volatility.framework.renderers.NotAvailableValue`. These indicate that data could not be read from the
|
||||
:py:class:`~volatility3.framework.renderers.UnreadableValue`,
|
||||
:py:class:`~volatility3.framework.renderers.UnparsableValue`,
|
||||
:py:class:`~volatility3.framework.renderers.NotApplicableValue` or
|
||||
:py:class:`~volatility3.framework.renderers.NotAvailableValue`. These indicate that data could not be read from the
|
||||
memory for some reason, could not be parsed properly, was not applicable or was not available.
|
||||
|
||||
A simple text renderer (that returns output immediately) would appear as follows. This doesn't use
|
||||
@@ -240,5 +240,5 @@ the accumulator, but instead uses print to directly produce the output. This is
|
||||
grid.populate(visitor, None)
|
||||
|
||||
More complex examples of renderers can be found in the default CLI implementation, such as the
|
||||
:py:class:`~volatility.cli.text_renderer.QuickTextRenderer` or the
|
||||
:py:class:`~volatility.cli.text_renderer.PrettyTextRenderer`.
|
||||
:py:class:`~volatility3.cli.text_renderer.QuickTextRenderer` or the
|
||||
:py:class:`~volatility3.cli.text_renderer.PrettyTextRenderer`.
|
||||
|
||||
@@ -6,7 +6,7 @@ Library and Context
|
||||
|
||||
Volatility 3 has been designed from the ground up to be a library, this means the components are independent and all
|
||||
state required to run a particular plugin at a particular time is self-contained in an object derived from
|
||||
a :py:class:`~volatility.framework.interfaces.context.ContextInterface`.
|
||||
a :py:class:`~volatility3.framework.interfaces.context.ContextInterface`.
|
||||
|
||||
The context contains the two core components that make up Volatility, layers of data and the available symbols.
|
||||
|
||||
@@ -14,7 +14,7 @@ Symbols and Types
|
||||
-----------------
|
||||
|
||||
Volatility 3 no longer uses profiles, it comes with an extensive library of
|
||||
:py:class:`symbol tables <volatility.framework.interfaces.symbols.SymbolTableInterface>`, and can generate new symbol
|
||||
:py:class:`symbol tables <volatility3.framework.interfaces.symbols.SymbolTableInterface>`, and can generate new symbol
|
||||
tables for most windows memory images, based on the memory image itself. This allows symbol tables to include specific
|
||||
offsets for locations (symbol locations) based on that operating system in particular. This means it is easier and quicker
|
||||
to identify structures within an operating system, by having known offsets for those structures provided by the official
|
||||
@@ -37,11 +37,11 @@ re-read many times over for no benefit (particularly since each re-read could re
|
||||
from following page table translations).
|
||||
|
||||
Finally, in order to provide Volatility specific information without impact on the ability for structures to have members
|
||||
with arbitrary names, all the metadata about the object (such as its layer or offset) have been moved to a read-only :py:meth:`~volatility.framework.interfaces.objects.ObjectInterface.vol`
|
||||
with arbitrary names, all the metadata about the object (such as its layer or offset) have been moved to a read-only :py:meth:`~volatility3.framework.interfaces.objects.ObjectInterface.vol`
|
||||
dictionary.
|
||||
|
||||
Further the distinction between a :py:class:`~volatility.framework.interfaces.objects.Template` (the thing that
|
||||
constructs an object) and the :py:class:`Object <volatility.framework.interfaces.objects.ObjectInterface>` itself has
|
||||
Further the distinction between a :py:class:`~volatility3.framework.interfaces.objects.Template` (the thing that
|
||||
constructs an object) and the :py:class:`Object <volatility3.framework.interfaces.objects.ObjectInterface>` itself has
|
||||
been made more explicit. In Volatility 2, some information (such as size) could only be determined from a constructed object,
|
||||
leading to instantiating a template on an empty buffer, just to determine the size. In Volatility 3, templates contain
|
||||
information such as their size, which can be queried directly without constructing the object.
|
||||
@@ -49,7 +49,7 @@ information such as their size, which can be queried directly without constructi
|
||||
Layer and Layer dependencies
|
||||
----------------------------
|
||||
Address spaces in Volatility 2, are now more accurately referred to as
|
||||
:py:class:`Translation Layers <volatility.framework.interfaces.layers.TranslationLayerInterface>`, since each one typically sits
|
||||
:py:class:`Translation Layers <volatility3.framework.interfaces.layers.TranslationLayerInterface>`, since each one typically sits
|
||||
atop another and can translate addresses between the higher logical layer and the lower physical layer. Address spaces in
|
||||
Volatility 2 were strictly limited to a stack, one on top of one other. In Volatility 3, layers can have multiple
|
||||
"dependencies" (lower layers), which allows for the integration of features such as swap space.
|
||||
@@ -65,13 +65,13 @@ included a stacker automagic to emulate the most common feature of Volatility 2,
|
||||
Searching and Scanning
|
||||
----------------------
|
||||
Scanning is very similar to scanning in Volatility 2, a scanner object (such as a
|
||||
:py:class:`~volatility.framework.layers.scanners.BytesScanner` or :py:class:`~volatility.framework.layers.scanners.RegExScanner`) is
|
||||
primed with the data to be searched for, and the :py:meth:`~volatility.framework.interfaces.layers.DataLayerInterface.scan` method is called on the layer to be searched.
|
||||
:py:class:`~volatility3.framework.layers.scanners.BytesScanner` or :py:class:`~volatility.framework.layers.scanners.RegExScanner`) is
|
||||
primed with the data to be searched for, and the :py:meth:`~volatility3.framework.interfaces.layers.DataLayerInterface.scan` method is called on the layer to be searched.
|
||||
|
||||
Output Rendering
|
||||
----------------
|
||||
This is extremely similar to Volatility 2, because we were developing it for Volatility 3 when we added it to Volatility 2.
|
||||
We now require that all plugins produce output in a :py:class:`~volatility.framework.interfaces.renderers.TreeGrid` object,
|
||||
We now require that all plugins produce output in a :py:class:`~volatility3.framework.interfaces.renderers.TreeGrid` object,
|
||||
which ensure that the library can be used regardless of which interface is driving it. An example web GUI is also available
|
||||
called Volumetric which allows all the plugins that can be run from the command line to be run from a webpage, and offers
|
||||
features such as automatic formatting and sorting of the data, which previously couldn't be provided easily from the CLI.
|
||||
|
||||
Reference in New Issue
Block a user