Skip to content

Plugin Development

In this guide we are going to learn how to develop a new plugin for Faraday.

Configure Custom Plugins

To add custom plugins in faraday, you first need to run this command:

faraday-manage settings -a update reports

Create Plugin structure

Create the folder new_plugin inside the custom_plugins folder and add the following files inside the new_plugin folder:

Plugin Structure:

new_plugin/
    __init__.py # leave this file empty
    plugin.py

Warning

Please respect both file names: __init__.py and plugin.py


Plugin code

There are two types of plugins:

  • File plugins (plugins that process the contents of a file)
  • Command plugins (plugins that process the execution of a command)

A faraday plugin can be either or both of these types. For example the ping plugin only process the execution of the command, but the nmap command supports both the xml generated by nmap or process the execution of nmap.

In your plugin.py file you must provide a function called createPlugin for the plugin manager to instantiate the plugin

def createPlugin(ignore_info=False, hostname_resolution=True):
    return YourPluginClass(ignore_info=ignore_info)

Plugin classes

PluginBase is the base class.

For File plugins we provide all this classes. But you can add other by creating a subclass of PluginByExtension

  • PluginBase
  • PluginByExtension
    • PluginXMLFormat
    • PluginJsonFormat
    • PluginMultiLineJsonFormat
    • PluginCSVFormat
    • PluginZipFormat

For Command plugins only you must inherit from PluginBase

Your plugin class __init__ method must define the id, that will be the identifier of your plugin.

Here's an example of the Ping tool plugin:

class CmdPingPlugin(PluginBase):
    def __init__(self, *arg, **kwargs):
        super().__init__(*arg, **kwargs)
            self.id = "ping"
            self.name = "Ping"
            self.plugin_version = "0.0.1"
            self.version = "1.0.0"
            self._command_regex = re.compile(r'^(sudo ping|ping|sudo ping6|ping6)\s+.*?')

Beware that it must be unique, you can list all the plugins identifiers with this command: faraday-plugins list-plugins

File Plugins

Each of the subclasses for File plugins provide a different way for the plugin to detect if it can process the file

PluginByExtension:

This class adds the functionality for identify a file by its extension by setting the attribute extension.

It can be one extension (".xml") or a list (['.xml', '.xxx']) if your file can have multiple extensions.

You never inherit directly from this class in a plugin but from some of its subclasses that are more specific.

class ExampleXMLTool(PluginByExtension):

        def __init__(self):
            super().__init__()
            self.extension = ".xml"
PluginXMLFormat:

Use this class if the plugin generates vulnerabilities from a xml file.

If your report has xml format but a different extension (like nessus), remember to define the extension attribute

To identify the file set in the identifier_tag attribute the main tag of the xml (ScanGroup in the example), it also can be one tag or a list of tags.

You can also set the identifier_tag_attributes attribute, it is a set of attributes that the main tag must have.

This is used to be more specific for example if the main tag is too generic and the xml of other plugin has the same one.

Example XML

<?xml version="1.0"?>
<ScanGroup ExportedOn="2021-11-05T11:18:57.673843">
    <Scan>
    </Scan>
</ScanGroup>

from urllib.parse import urlparse
from faraday_plugins.plugins.plugin import PluginXMLFormat
import xml.etree.ElementTree as ET

class ExampleToolXmlParser:

    def __init__(self, xml_output):
        self.vulns = self.parse_xml(xml_output)

    def parse_xml(self, xml_output):
        vulns = []
        tree = ET.fromstring(xml_output)
        items = tree.iterfind('details/item')
        for item in items:
            ip = item.get('ip')
            os = item.get('os')
            uri = item.find('uri').text
            url = urlparse(uri)
            hostname = [url.netloc]
            path = url.path
            if url.scheme == 'https':
                port = 443
            else:
                port = 80
            issue = item.find('issue')
            severity = issue.get('severity')
            issue_text = issue.text
            vuln = {'ip': ip, 'uri': uri, 'os': os,
                    'hostname': hostname, 'port': port, 'path': path,
                    'issue_text': issue_text, 'severity': severity}
            vulns.append(vuln)
        return vulns


class ExampleToolPlugin(PluginXMLFormat):
    def __init__(self, *arg, **kwargs):
        super().__init__(*arg, **kwargs)
        self.identifier_tag = "example_tool"
        self.id = "example_tool"
        self.name = "Name of the tool"
        self.plugin_version = "0.0.1"

    def parseOutputString(self, output, debug=False):
        parser = ExampleToolXmlParser(output)
        for vuln in parser.vulns:
            h_id = self.createAndAddHost(vuln['ip'], vuln['os'], hostnames=vuln['hostname'])
            s_id = self.createAndAddServiceToHost(h_id, 'webserver', protocol='tcp', ports=vuln['port'])
            v_id = self.createAndAddVulnWebToService(h_id, s_id, vuln['issue_text'], severity=vuln['severity'],
                                                    path=vuln['path'])

def createPlugin(*args, **kwargs):
    return ExampleToolPlugin(*args, **kwargs)
PluginJsonFormat:

Use this class if the plugin generates vulnerabilities from a json file.

If yor report has json format but a different extension (not .json), remember to define the extension attribute

To identify the file set in the json_keys attribute a set with some identifiers keys of the json object

Example JSON

{
  "name": "some name",
  "hosts": ["host1", "host2"],
  "other_field": "some value"
}

class ExampleJsonTool(PluginJsonFormat):
    def __init__(self, ignore_info):
        super().__init__(ignore_info)
        self.json_keys = {'name', 'hosts'}
PluginMultiLineJsonFormat:

Is the same as PluginJsonFormat but use it when the file has multiple lines and every line is a json.

Example JSON

{ "name": "some name", "hosts": ["host1", "host2"],"other_field": "some value"}
{ "name": "other name", "hosts": ["host3", "host4"],"other_field": "other value"}

class ExampleMultipleJsonTool(PluginMultiLineJsonFormat):
    def __init__(self, ignore_info):
        super().__init__(ignore_info)
        self.json_keys = {'name', 'hosts'}
PluginCSVFormat:

Use this class if the plugin generates vulnerabilities from a csv file.

If yor report has csv format but a different extension (not .csv), remember to define the extension attribute

To identify the file set in the csv_headers attribute a set with some identifiers headers of the csv object

Example CSV

name,ip,os
host1,10.10.10.10,windows
host2,10.10.10.11,linux

class ExampleCSVTool(PluginCSVFormat):
    def __init__(self, ignore_info):
        super().__init__(ignore_info)
        self.json_keys = {'name', 'ip'}
PluginZipFormat:

Use this class if the plugin generates vulnerabilities from a zip file.

If yor report has csv format but a different extension (not .zip), remember to define the extension attribute

To identify the file set in the files_list attribute a set with some file names inside the zip file.

class ExampleJsonTool(PluginZipFormat):
    def __init__(self, ignore_info):
        super().__init__(ignore_info)
        self.files_list = {'file1.txt', 'file2.txt'}
Generating data

The PluginBase class give you all the main methods to create hosts, services and vulnerabilities

After you parse the data you will need to create the objects with the information to send to faraday. This are the main methods

def createAndAddHost(self, name, os="unknown", hostnames=None, mac=None, scan_template="", site_name="",
                    site_importance="", risk_score="", fingerprints="", fingerprints_software=""):

def createAndAddServiceToHost(self, host_id, name, protocol="tcp?", ports=None, status="open", version="unknown",
                            description=""):

def createAndAddVulnToHost(self, host_id, name, desc="", ref=None, severity="", resolution="", vulnerable_since="", 
                            scan_id="", pci="", data="", external_id=None, run_date=None):

def createAndAddVulnToService(self, host_id, service_id, name, desc="", ref=None, severity="", resolution="", 
                            risk="", data="", external_id=None, run_date=None):

def createAndAddVulnWebToService(self, host_id, service_id, name, desc="", ref=None, severity="", resolution="",
                                website="", path="", request="", response="", method="", pname="",
                                params="", query="", category="", data="", external_id=None, run_date=None):

createAndAddHost and createAndAddServiceToHost will give you a host ID and a service ID. You need to send those to the create vulnerabilities methods

The main method to parse data and create the vulnerabilities objects is parseOutputString, no mather if is a File or a Command plugin.

Command Plugins

For command plugins the detection is done via regex.

If the plugin is only for command you inherit from PluginBase if it also will support file detection inherit from the appropriate class for that type of file.

In both cases to detect a command you will need to set the _command_regex attribute with the regex to match the command

Here's an example of the Ping tool plugin:

class CmdPingPlugin(PluginBase):
    def __init__(self, *arg, **kwargs):
        super().__init__(*arg, **kwargs)
            self.id = "ping"
            self.name = "Ping"
            self.plugin_version = "0.0.1"
            self.version = "1.0.0"
            self._command_regex = re.compile(r'^(sudo ping|ping|sudo ping6|ping6)\s+.*?')

    def parseOutputString(self, output):
        reg = re.search(r"PING ([\w\.-:]+)( |)\(([\w\.:]+)\)", output)
        if re.search("0 received|unknown host", output) is None and reg is not None:
            ip_address = reg.group(3)
            hostname = reg.group(1)
            self.createAndAddHost(ip_address, hostnames=[hostname])
        return True

    def _isIPV4(self, ip):
        if len(ip.split(".")) == 4:
            return True
        else:
            return False

In this case the plugin manager will run the command and send the output to the parseOutputString method.

But if the tool generate an ouput file instead of send the data to stdout you will need to check that the command has the required parameters to do that.

To do that you will need to implement the processCommandString method, by default it will return the command without modifications.

To use output files you will need to set 2 attributes in you plugin: _use_temp_file and _temp_file_extension

If _use_temp_file is set the plugin will create a temp file and assign its path to the attribute self._output_file_path of the plugin.

If you need that the temp file has and specific extension use the _temp_file_extension attribute like in the example.

If the _use_temp_file attribute is set the plugin will send te content of that file to parseOutputString()instead of the output of the command.

Here is and extract of the nmap plugin to see how processCommandString will modify the original command and return a new command with the required parameters.

class NmapPlugin(PluginXMLFormat):
    """
    Example plugin to parse nmap output.
    """

    def __init__(self, *arg, **kwargs):
        super().__init__(*arg, **kwargs)
        self.identifier_tag = "nmaprun"
        self.id = "Nmap"
        self.name = "Nmap XML Output Plugin"
        self.plugin_version = "0.0.3"
        self.version = "6.40"
        self.framework_version = "1.0.0"
        self.options = None
        self._current_output = None
        self._command_regex = re.compile(r'^(sudo nmap|nmap|\.\/nmap)\s+.*?')
        self._use_temp_file = True
        self._temp_file_extension = "xml"
        self.xml_arg_re = re.compile(r"^.*(-oX\s*[^\s]+).*$")
        self.addSetting("Scan Technique", str, "-sS")

    def parseOutputString(self, output):
        ...

    def processCommandString(self, username, current_path, command_string):
        """
        Adds the -oX parameter to get xml output to the command string that the
        user has set.
        """
        super().processCommandString(username, current_path, command_string)
        arg_match = self.xml_arg_re.match(command_string)
        if arg_match is None:
            return re.sub(r"(^.*?nmap)",
                          r"\1 -oX %s" % self._output_file_path,
                          command_string)
        else:
            return re.sub(arg_match.group(1),
                          r"-oX %s" % self._output_file_path,
                          command_string)

Full Example

This is an example of a Faraday Plugin that process a xml report.

In this example we will create a plugin to analyze this XML provided by the output of a tool.

example.xml

 <?xml version="1.0" ?>
<!DOCTYPE example_tool>
<example_tool scanstart="Thu Nov  9 15:59:13 2017">
    <details>
    <item id="999979" ip="10.23.49.232" os="linux">
    <uri>http://test.com/example.php</uri>
    <issue severity="low">Some vuln text</issue>
    </item>
    <item id="39023023" ip="10.232.62.20" os="linux">
    <uri>http://test.com/login.php</uri>
    <issue severity="low">Some other text</issue>
    </item>
    <item id="8348343" ip="10.12.37.24" os="linux">
    <uri>http://test.com/example.php</uri>
    <issue severity="low">Yet another vuln text</issue>
    </item>
    <statistics elapsed="402" itemsfound="3" itemstested="10" />
    </details>
</example_tool>

plugin.py

from urllib.parse import urlparse
from faraday_plugins.plugins.plugin import PluginXMLFormat
import xml.etree.ElementTree as ET

class ExampleToolXmlParser:

    def __init__(self, xml_output):
        self.vulns = self.parse_xml(xml_output)

    def parse_xml(self, xml_output):
        vulns = []
        tree = ET.fromstring(xml_output)
        items = tree.iterfind('details/item')
        for item in items:
            ip = item.get('ip')
            os = item.get('os')
            uri = item.find('uri').text
            url = urlparse(uri)
            hostname = [url.netloc]
            path = url.path
            if url.scheme == 'https':
                port = 443
            else:
                port = 80
            issue = item.find('issue')
            severity = issue.get('severity')
            issue_text = issue.text
            vuln = {'ip': ip, 'uri': uri, 'os': os,
                    'hostname': hostname, 'port': port, 'path': path,
                    'issue_text': issue_text, 'severity': severity}
            vulns.append(vuln)
        return vulns


class ExampleToolPlugin(PluginXMLFormat):
    def __init__(self):
        super().__init__()
        self.identifier_tag = "example_tool"
        self.id = "example_tool"
        self.name = "Name of the tool"
        self.plugin_version = "0.0.1"

    def parseOutputString(self, output, debug=False):
        parser = ExampleToolXmlParser(output)
        for vuln in parser.vulns:
            h_id = self.createAndAddHost(vuln['ip'], vuln['os'], hostnames=vuln['hostname'])
            s_id = self.createAndAddServiceToHost(h_id, 'webserver', protocol='tcp', ports=vuln['port'])
            v_id = self.createAndAddVulnWebToService(h_id, s_id, vuln['issue_text'], severity=vuln['severity'],
                                                    path=vuln['path'])

def createPlugin(ignore_info=False, hostname_resolution=True):
    return ExampleToolXmlParser(ignore_info=ignore_info)

Test and Debug

You can test your plugin by enabling the custom_plugins_folder setting, and try it with faraday.

You can also test your plugin from the command line.

Test Plugins

List all available plugins

Verify that the plugins is loaded by the plugin manager

faraday-plugins list-plugins

Available Plugins:
...
...
...
example_tool - Name of the tool
Loaded Plugins: 84

Test plugin report detection

Verify that your file is detected by your plugin

faraday-plugins detect-report /path/to/report.xml

Plugin: example_tool

Test plugin process report

Verify that your plugin parses the file ok and generate the json structure that will be loaded into faraday

faraday-plugins process-report /path/to/report.xml


{"hosts": [{"ip": "10.23.49.232", "os": "linux", "hostnames": ["test.com"], "description": "", "mac": null, "credentials": [], "services": [{"name": "webserver", "protocol": "tcp", "port": 80, "status": "open", "version": "unknown", "description": "", "credentials": [], "vulnerabilities": [{"name": "Some vuln text", "desc": "", "severity": "low", "refs": [], "external_id": null, "type": "VulnerabilityWeb", "resolution": "", "data": "", "website": "", "path": "/example.php", "request": "", "response": "", "method": "", "pname": "", "params": "", "query": "", "category": ""}]}], "vulnerabilities": [], "scan_template": "", "site_name": "", "site_importance": "", "risk_score": "", "fingerprints": "", "fingerprints_software": ""}, {"ip": "10.232.62.20", "os": "linux", "hostnames": ["test.com"], "description": "", "mac": null, "credentials": [], "services": [{"name": "webserver", "protocol": "tcp", "port": 80, "status": "open", "version": "unknown", "description": "", "credentials": [], "vulnerabilities": [{"name": "Some other text", "desc": "", "severity": "low", "refs": [], "external_id": null, "type": "VulnerabilityWeb", "resolution": "", "data": "", "website": "", "path": "/login.php", "request": "", "response": "", "method": "", "pname": "", "params": "", "query": "", "category": ""}]}], "vulnerabilities": [], "scan_template": "", "site_name": "", "site_importance": "", "risk_score": "", "fingerprints": "", "fingerprints_software": ""}, {"ip": "10.12.37.24", "os": "linux", "hostnames": ["test.com"], "description": "", "mac": null, "credentials": [], "services": [{"name": "webserver", "protocol": "tcp", "port": 80, "status": "open", "version": "unknown", "description": "", "credentials": [], "vulnerabilities": [{"name": "Yet another vuln text", "desc": "", "severity": "low", "refs": [], "external_id": null, "type": "VulnerabilityWeb", "resolution": "", "data": "", "website": "", "path": "/example.php", "request": "", "response": "", "method": "", "pname": "", "params": "", "query": "", "category": ""}]}], "vulnerabilities": [], "scan_template": "", "site_name": "", "site_importance": "", "risk_score": "", "fingerprints": "", "fingerprints_software": ""}], "command": {"tool": "example_tool", "command": "example_tool", "params": "/path/to/report.xml", "user": "faraday", "hostname": "", "start_date": "2020-04-01T18:43:34.552623", "duration": 2650, "import_source": "report"}}

You can optionally specify the plugin id to not do the detection step and force to process it with the specific plugin

faraday-plugins process-report --plugin_id YourPluginId /path/to/report.xml


{"hosts": [{"ip": "10.23.49.232", "os": "linux", "hostnames": ["test.com"], "description": "", "mac": null, "credentials": [], "services": [{"name": "webserver", "protocol": "tcp", "port": 80, "status": "open", "version": "unknown", "description": "", "credentials": [], "vulnerabilities": [{"name": "Some vuln text", "desc": "", "severity": "low", "refs": [], "external_id": null, "type": "VulnerabilityWeb", "resolution": "", "data": "", "website": "", "path": "/example.php", "request": "", "response": "", "method": "", "pname": "", "params": "", "query": "", "category": ""}]}], "vulnerabilities": [], "scan_template": "", "site_name": "", "site_importance": "", "risk_score": "", "fingerprints": "", "fingerprints_software": ""}, {"ip": "10.232.62.20", "os": "linux", "hostnames": ["test.com"], "description": "", "mac": null, "credentials": [], "services": [{"name": "webserver", "protocol": "tcp", "port": 80, "status": "open", "version": "unknown", "description": "", "credentials": [], "vulnerabilities": [{"name": "Some other text", "desc": "", "severity": "low", "refs": [], "external_id": null, "type": "VulnerabilityWeb", "resolution": "", "data": "", "website": "", "path": "/login.php", "request": "", "response": "", "method": "", "pname": "", "params": "", "query": "", "category": ""}]}], "vulnerabilities": [], "scan_template": "", "site_name": "", "site_importance": "", "risk_score": "", "fingerprints": "", "fingerprints_software": ""}, {"ip": "10.12.37.24", "os": "linux", "hostnames": ["test.com"], "description": "", "mac": null, "credentials": [], "services": [{"name": "webserver", "protocol": "tcp", "port": 80, "status": "open", "version": "unknown", "description": "", "credentials": [], "vulnerabilities": [{"name": "Yet another vuln text", "desc": "", "severity": "low", "refs": [], "external_id": null, "type": "VulnerabilityWeb", "resolution": "", "data": "", "website": "", "path": "/example.php", "request": "", "response": "", "method": "", "pname": "", "params": "", "query": "", "category": ""}]}], "vulnerabilities": [], "scan_template": "", "site_name": "", "site_importance": "", "risk_score": "", "fingerprints": "", "fingerprints_software": ""}], "command": {"tool": "example_tool", "command": "example_tool", "params": "/path/to/report.xml", "user": "faraday", "hostname": "", "start_date": "2020-04-01T18:43:34.552623", "duration": 2650, "import_source": "report"}}

If you do not have faraday-server installed or don't have the custom_plugins_folder setting, you can use the --custom_plugins_folder parameter with any if the commands (list, detect and process)

Example:

faraday-plugins list-plugins --custom-plugins-folder /home/user/.faraday/plugins/
Logging

In the PluginBase there is a logger defined in self.logger that you can use.

If you need to debug for plugins with the command line set this variable:

export PLUGIN_DEBUG=1
faraday-plugins process-report appscan /path/to/report.xml
2019-11-15 20:37:03,355 - faraday.faraday_plugins.plugins.manager - INFO [manager.py:113 - _load_plugins()]  Loading Native Plugins...
2019-11-15 20:37:03,465 - faraday.faraday_plugins.plugins.manager - DEBUG [manager.py:123 - _load_plugins()]  Load Plugin [acunetix]
2019-11-15 20:37:03,495 - faraday.faraday_plugins.plugins.manager - DEBUG [manager.py:123 - _load_plugins()]  Load Plugin [amap]
2019-11-15 20:37:03,549 - faraday.faraday_plugins.plugins.manager - DEBUG [manager.py:123 - _load_plugins()]  Load Plugin [appscan]
2019-11-15 20:37:03,580 - faraday.faraday_plugins.plugins.manager - DEBUG [manager.py:123 - _load_plugins()]  Load Plugin [arachni]
2019-11-15 20:37:03,613 - faraday.faraday_plugins.plugins.manager - DEBUG [manager.py:123 - _load_plugins()]  Load Plugin [arp_scan]
2019-11-15 20:37:03,684 - faraday.faraday_plugins.plugins.manager - DEBUG [manager.py:123 - _load_plugins()]  Load Plugin [beef]
2019-11-15 20:37:03,714 - faraday.faraday_plugins.plugins.manager - DEBUG [manager.py:123 - _load_plugins()]  Load Plugin [brutexss]
2019-11-15 20:37:03,917 - faraday.faraday_plugins.plugins.manager - DEBUG [manager.py:123 - _load_plugins()]  Load Plugin [burp]
2019-11-15 20:37:03,940 - faraday.faraday_plugins.plugins.manager - DEBUG [manager.py:123 - _load_plugins()]  Load Plugin [dig]
...