Plugin Development¶
Overview¶
There are several plugin categories that are available currently:
- worker
- connector
- reader
- source
- extractor
- carver
- decoder
- decorator
The worker plugin category is for the plugins that will produce data from
payloads and provide the results back to the framework for output. Once the
worker plugin is complete, the framework will want to handle the results in
some fashion. This is where the connector plugins come into play. Once the
results have been provided back to the framework, the connector is then called.
The connector plugins may also be used for file archiving if supported
within the plugin. One may save a file using a connector by simply calling
save()
with the archive=True
option. Conversely, in order to retrieve a
file from MongoDB’s GridFS, one simply would call get_file()
. Reader
plugins are used to enrich data for worker plugins such as indicator
extraction, STIX support, or a multitude of other enhancements. Source
plugins handle the messaging and queueing of objects that the worker should
handle. For instance, monitoring a directory for new files or AMQP.
Extractor plugins handle various tasks such as decompressing zip files and
deflating pdf streams. Carver plugins are used to carve content out of
payloads (e.g., SWF streams out of DOC files, PE out of RTF, etc…).
Decoder plugins provide the capability to automatically decode a payload,
such as XOR, ROR, and base64. Decorator plugins allow for post processing
of results from stoQ before being saved or returned.
Configuration¶
Each plugin has it’s own configuration file ending in .stoq. Upon initialization of the plugin, the configuration options within the file will be loaded and made available to the worker object.
At a minimum, the below configuration options are required for all plugins.
[Core]
# Name of plugin. stoQ will use this when calling the plugin.
Name = basicplugin
# Name of the .py file for this plugin
Module = basicplugin
[Documentation]
Author = Joe Stoq
Version = 0.1
Website = https://github.com/PUNCH-Cyber/stoq
Description = Basic Plugin Example
If a plugin requires additional configuration parameters, they can be added to
the [options]
section and will be made available via the plugin object. For
example, if we have defined our plugin object as plugin
, we can access the
hashpayload
attribute by calling self.hashpayload
.
[options]
hashpayload = True
saveresults = True
max_tlp = red
max_stoq_version = 0.10.3
min_stoq_version = 0.9
ratelimit = 1/5
Note
As of stoQ version 0.10.3, plugin version checking is supported. If the min/max version of stoQ is not met, processing of the payload will proceed, but the user will be warned unpredictable results may be encountered.
Note
Worker plugins require the hashpayload
and saveresults
configuration options. No other plugins have additional requirements.
Note
Worker plugin supports a max_tlp
option, which will limit
it’s ability to scan a payload based on the TLP level of the
payload itself. Valid options are red, amber, green, and white. More
information on TLP levels can be found at https://www.us-cert.gov/tlp
Note
Worker plugins support rate limiting. The value for ratelimit
should be in the form of “count/per seconds”. For example, the value
1/10
would mean stoQ will processes 1
sample every
10 seconds
.
Plugin Development¶
A Worker plugin extends the StoqWorkerPlugin
class. As such, it must
inherit the StoqWorkerPlugin
class when initialized. In order to function
properly, there must be several methods defined within the worker plugin.
- __init__
- activate
The __init__
method is called upon initialization of the plugin. This
occurs when the Stoq.load_plugin
method is called with the plugin name or
when Stoq.collect_plugins
plugins is called.
The activate
method is automatically called after the plugin has been
initialized. When it is called, it must have stoq
as an attribute. This
allows the plugin to have full access to the stoQ framework and configuration
options. The activate
method should only be called once by the framework
upon initialization. Any initial configuration and command line options should
be placed here. This method must also return True
in order for the
framework to continue, otherwise stoQ will assume that the plugin activation
has failed.
Additionally, the deactivate
method is called when/if the plugin is ever
deactivated, including when stoQ shuts down. This method is not required,
though it is recommended should the plugin have any actions that need to
cleaning up or if stoQ needs to deactivate the plugin for any reason.
For each of the above core methods, they should minimally call
super().METHOD_NAME()
right before they return. METHOD_NAME should be
changed to the respective method. This will allow the respective parent class
execute any required code.
For time-based events (periodic flushes of buffers, etc), every plugin can
define a wants_heartbeat
property of the plugin. If that property is True,
then a separate thread will be launched by stoQ to call the plugin’s heartbeat
method. The heartbeat
method will be called with the plugin object as its
only argument (so heartbeat
can be treated as a class method of the plugin).
The heartbeat
method will only be called once, and it is expected to loop
to call whatever periodic actions the plugin wishes to take. For example
def heartbeat(self):
while True:
time.sleep(1)
self._checkCommit()
Note
Actions performed in the heartbeat must be multithread/multiprocess
safe. If the actions in the heartbeat may change the values of properties
that other plugin methods (like save
) may also change, it is the responsibility
of the plugin to properly handle locking access to those objects, or find other
methods of thread safety.
Note
Also, at present only Worker and Connector plugins are checked to see if they need heartbeats. Others may be added in the future if the need arises.
Workers¶
In addition to the above requirements, the below method is required for Worker plugins:
- scan
The scan
method is called when command stoq
command has a payload available for
processing. scan
requires two attributes, payload
and **kwargs
.
payload
is the payload that the plugin should process. If the plugin does
not require a payload, payload
will be None
. **kwargs
is a
dict
that contains the message provide by RabbitMQ, or some basic
metadata if RabbitMQ is not utilized. Once the scan
method has completed
processing the payload, it should return it’s results as a dict
or list
.
If results are returned as a list
, each item in the list
will be processed
separately by the StoqConnectorPlugin
. This will result in multiple results
being saved separately for each payload. This allows for worker plugins to save
results without making multiple calls, such as when interacting with an API that
returns multiple results or parsing an SMTP session that contains a stream of
e-mails. Optionally, if the results do not need to be process, it can return
None
.
Below is an example of a basic worker plugin.
# Required imports
import argparse
from stoq.args import StoqArgs
from stoq.plugins import StoqWorkerPlugin
# The worker plugin class must be unique. It will be inheriting
# the StoqWorkerPlugin class.
class BasicWorker(StoqWorkerPlugin):
def __init__(self):
# In nearly all cases, we do not want to handle anything here
super().__init__()
# This function is required in order to initialize the worker.
# The framework will call the activate() function upon initialization
# and must return True in order for the framework to continue
def activate(self, stoq):
# Ensure the stoQ class is available throughout the
# plugin
self.stoq = stoq
# Instantiate our workers command line argument parser
parser = argparse.ArgumentParser()
# Initialize the default requirements for a worker, if needed.
parser = StoqArgs(parser)
# Define the argparse group for this plugin
worker_opts = parser.add_argument_group("Plugin Options")
# Define the command line arguments for the worker
worker_opts.add_argument("-r", "--rules",
dest='rulepath',
help="Path to rules file.")
# The first command line argument is reserved for the framework.
# The work should only parse everything after the first command
# line argument. We must always use stoQ's argv object to ensure
# the plugin is properly instantied whether it is imported or
# used via a command line script
options = parser.parse_args(self.stoq.argv[2:])
# If we need to handle command line argument, let's pass them
# to super().activate so they can be instantied within the worker
super().activate(options=options)
# Must return true, otherwise the framework believes something
# went wrong
return True
# The framework will call the scan() function when it is ready to
# scan. All of the initial functionality should reside here
def scan(self, payload, **kwargs):
# Must return a dict
kwargs['err'] = "Need more to do!"
return kwargs
Note
super().activate(options=options)
must be called for the plugin
to be fully initialized.
Connectors¶
In addition to the above requirements, the below methods are required for Connector plugins
- save
The save
method is called to save a payload to the specified connector. It
must have the payload
and **kwargs
attributes. The payload
attribute should be the data that will be saved via the connector. **kwargs
are any additional attributes that the method may require.
Optionally, the below methods can be provided.
- connect
- disconnect
- get_file
connect
should be called when a connection, or reconnection, to the
connector database is required. Ideally, logic should be placed in save
that will call connect
to verify a live connection still exists.
disconnect
is called when the connector should cleanly disconnect from the
database.
get_file
is used if the database supports the saving of files. get_file
may be used to retrieve any files that are saved to the connector. The
**kwargs
attribute should contain whatever datapoints are need to retrieve
the file.
from stoq.plugins import StoqConnectorPlugin
class BasicConnector(StoqConnectorPlugin):
def __init__(self):
super().__init__()
def activate(self, stoq):
self.stoq = stoq
# Any additonal requirements once the connector is activated
# should be placed here
super().activate()
def get_file(self, **kwargs):
# Code to retrieve file from this connector should be placed here
# No results, carry on.
return None
def save(self, payload, **kwargs):
"""
Save results to mongodb
:param str payload: Content to be inserted into database
:param dict **kwargs: Any additional attributes that should
be added to the GridFS object on insert
"""
# Make sure we have a valid connection
self.connect()
# Code to handle saving of the results should be placed here
super().save()
def connect(self, force_connect=False):
# Logic should reside here that determines if we have an
# active/valid connection, and if not, make one. Otherwise
# continue on so the framework can save it's results.
super().connect()
def disconnect(self):
super().disconnect()
Readers¶
In addition to the above requirements, the below method is required for Reader plugins:
- read
The read
method requires the payload
attribute, and optionally
**kwargs
. The payload
should be the content that the Reader plugin
should process. Any additional attributes should be defined in **kwargs
.
Once the Reader plugin is done processing the payload
, it should return
its results.
from stoq.plugins import StoqReaderPlugin
class BasicReader(StoqReaderPlugin):
def __init__(self):
super().__init__()
def activate(self, stoq):
self.stoq = stoq
super().activate()
def read(self, payload, **kwargs):
"""
Basic Reader
:param bytes payload: Payload to be processed
:returns: Content of payload
"""
return payload
Sources¶
In addition to the above requirements, the below methods are required for Source plugins:
- ingest
The ingest
method does not require any arrtributes when called. Source
plugins should push data back to the worker by calling the
worker.multiprocess_put
method. This is will pull data back to the
main method for processing data in and our of the framework to include
retrieving payloads, hashing, metadata generation, result handling, and saving
of results.
from stoq.plugins import StoqSourcePlugin
class FileSource(StoqSourcePlugin):
def __init__(self):
super().__init__()
def activate(self, stoq):
self.stoq = stoq
super().activate()
def ingest(self):
path = "/tmp/bad.exe"
self.stoq.worker.multiprocess_put(path=path, archive='file')
return True
A source plugin also requires the multiprocess
boolean configuration
option in it’s .stoq
file under the [options] header. For example:
[options]
multiprocess = True
If set to True
, the source plugin will be capable of being run with
multiple instances simultaneously. Note: if multiprocess
option is
set to False
the source will still be run in a Python process, but
stoq will only run one instance of that process.
Extractors¶
In addition to the above requirements, the below methods are required for Extractor plugins:
- extract
extract()
must be called with the payload
parameter. Optionally,
**kwargs
may be provided. The plugin may return None or a list of tuples.
Index 0 of the tuple must be a dict() containing metadata associated with
the decoded content, and Index 1 must be the decoded content itself as bytes.
from stoq.plugins import StoqExtractorPlugin
class ExampleExtractor(StoqExtractorPlugin):
def __init__(self):
super().__init__()
def activate(self, stoq):
self.stoq = stoq
super().activate()
def extract(self, payload, **kwargs):
# handle any extraction requirements here
meta = {"size": len(payload), "type": "test"}
return [(meta, payload)]
Carvers¶
In addition to the above requirements, the below methods are required for Carver plugins:
- carve
carve()
must be called with the payload
parameter. Optionally,
**kwargs
may be provided. The plugin may return None or a list of tuples.
Index 0 of the tuple must be a dict() containing metadata associated with
the decoded content, and Index 1 must be the decoded content itself as bytes.
from stoq.plugins import StoqCarverPlugin
class ExampleCarver(StoqExtractorPlugin):
def __init__(self):
super().__init__()
def activate(self, stoq):
self.stoq = stoq
super().activate()
def carve(self, payload, **kwargs):
# handle any carving requirements here
meta = {"size": len(payload), "type": "test"}
return [(meta, payload)]
Decoders¶
In addition to the above requirements, the below methods are required for Decoder plugins:
- decode
decode()
must be called with the payload
parameter. Optionally,
**kwargs
may be provided. The plugin may return None or a list of tuples.
Index 0 of the tuple must be a dict() containing metadata associated with
the decoded content, and Index 1 must be the decoded content itself as bytes.
from stoq.plugins import StoqDecoderPlugin
class ExampleDecoder(StoqDecoderPlugin):
def __init__(self):
super().__init__()
def activate(self, stoq):
self.stoq = stoq
super().activate()
def decode(self, payload, **kwargs):
# handle any decoding requirements here
meta = {"size": len(payload), "type": "test"}
return [(meta, payload)]
Decorators¶
In addition to the above requirements, the below methods are required for Decorator plugins:
- decorate
decorate()
must be called with the results
parameter. The plugin must
return a dict
of the original results provided to it, or modified results
.
Note
The dict
returned from decorate()
WILL be what is saved/returned
from stoQ, so be extremely careful with how results is modified.
from stoq.plugins import StoqDecoratorPlugin
class ExampleDecorator(StoqDecoratorPlugin):
def __init__(self):
super().__init__()
def activate(self, stoq):
self.stoq = stoq
super().activate()
def decorate(self, results):
# handle any logic to determine what is added to results here
if 'APT' in results['scan']:
results = {'apt_malware': True}
return results
Packaging Plugins¶
stoQ provides a method to install plugins and their dependencies utilzing setuptool and pip. In order to leverage the plugin installation feature, some requirements must be met for the plugin package.
- The plugin package must be a directory
- The plugin directory must have a subdirectory by the same name as defined in the plugins
.stoq
configuration file- The plugin directory must contain a valid stoQ configuration file
- The plugin directory must contain a valid stoQ plugin
- The plugin directory must contain a file named
__init__.py
- Optionally, the archive/directory may contain a valid pip requirements.txt file. The pip packages within this file will automatically be installed with the stoQ plugin.
- Optionally, a MANIFEST.in file can be included to define which files within the package should be copied to the installation path.
Note
The plugin’s configuration file will not be copied by default, this
file should either be defined here or within package_data
in
setup.py
.
As an example, a stoQ plugin archive should have the following structure:
basicworker-plugin/
setup.py
MANIFEST.in (optional)
requirements.txt (optional)
basicworker/
__init__.py
basicworker.stoq
basicworker.py
The stoQ installation process will extract plugin options from it’s .stoq
configuration file. As such, the plugin’s setup.py
file should be fairly
simple. The below setup.py
should suffice for most plugins.:
from setuptools import setup, find_packages
setup(
name=open("NAME").read(),
version=open("VERSION").read(),
author=open("AUTHOR").read(),
url=open("WEBSITE").read(),
license="Apache License 2.0",
description=open("DESCRIPTION").read(),
packages=find_packages(),
include_package_data=True,
classifiers=[
"Development Status :: 3 - Alpha",
"Topic :: Utilities",
],
)
Templates¶
stoQ allows for two types of outputs. First, a JSON blob that can be easily parsed in an automated fashion. In addition, stoQ can handle output using Jinja2 templating. This allows for highly customizable and human readable output that may be neccessary in many circumstances. As an example, when using the slack worker plugin, it is not ideal to have hundreds, maybe even thousands, of lines sent to a channel as a result of scanning a payload. With stoQ’s templating engine, one can easily send human readable and easily digested results to the Slack channel, while at the same time providing the JSON results to a connector for saving into a database for later use.
Using stoQ’s templates is a simple process. Simply create a templates
directory in the plugin’s directory and then create a new template
file
in Jinja2 format. For example, let’s say we have a worker plugin by the
name peinfo. We want to create a Slack template for this plugin.
All that is needed now is for a slack.tpl
template to be placed in this
directory. Now, we just need to run the slack worker with the -T slack.tpl
argument. The slack worker plugin will then load the template and render the
results.
Additionally, content that is passed to the connector plugin may also be parsed
using the templating engine. In order to use this functionality, the worker
plugin that is producing the data must have a template named after the connector
plugin that is being utilized. For instance, if one would like to ensure the
stdout connector output is human readable and not the JSON results, simply
create a new template with the name stdout.tpl
and call the worker with
-T stdout.tpl
.
Installing a Plugin¶
Installation of a stoQ plugin is very simple. Let’s assume that we want to install the basicworker plugin that comes prepackaged with stoQ. We must first package the plugin as detailed above, and then run the command from within the stoQ directory:
stoq install basicworker-plugin
.d8888b. 888 .d88888b.
d88P Y88b 888 d88P" "Y88b
Y88b. 888 888 888
"Y888b. 888888 .d88b. 888 888
"Y88b. 888 d88""88b 888 888
"888 888 888 888 888 Y8b 888
Y88b d88P Y88b. Y88..88P Y88b.Y8b88P
"Y8888P" "Y888 "Y88P" "Y888888"
Y8b
[+] Looking for plugin in /vagrant/stoq/plugin-packages/worker/yara...
[+] Installing yara plugin into /vagrant/stoq/stoq/plugins/worker...
[+] Install complete.
Let’s examine what stoQ just did. First, we opened the basicworker-plugin plugin package and began searching for a stoQ plugin configuration file. Once it was found, we loaded it and searched for the Name and Module configuration options within the file. That allowed us to discover the plugin name along with the plugins .py filename. stoQ then discovered the plugin class to determine the full path where the plugin should be installed to. It then called pip to complete the installation.
If a file or directory exists, it will not be overwritten. Instead, a warning message will be displayed letting the user know that the plugin will not be installed. In order to successfully install the plugin, the file or directory must be removed, renamed, or –upgrade be called at the command line.