close
Skip to content

Plugin Development Guide

Overview

Faraday ships with 118+ built-in plugins that parse output from tools such as Nmap, Burp Suite, Nessus, Nuclei, and many more. When the tool you use is not already covered, you can write a custom plugin and drop it into a designated folder so Faraday loads it alongside the built-in ones.

There are two kinds of plugins:

Kind Description Base class
File / Report plugin Parses a file (XML, JSON, CSV, ZIP, …) produced by a tool PluginByExtension subclass
Command plugin Intercepts a shell command, runs it, and parses its stdout/output file PluginBase (with _command_regex)

A plugin can support both modes at the same time (e.g., Nmap parses XML reports and intercepts the nmap command).


1 — Prerequisites

Install the faraday-plugins package:

pip install faraday-plugins

Key dependencies installed automatically: lxml, beautifulsoup4, simplejson, html2text, python-dateutil, pandas, tldextract.


2 — Configure Custom Plugins

In Faraday Server

Run the following command to enable the custom-plugins folder in Faraday's settings:

faraday-manage settings -a update reports

This tells Faraday where to look for your custom plugins. The default custom plugins path is ~/.faraday/plugins/.

Standalone (no server required)

You can also develop and test plugins entirely from the command line by passing --custom-plugins-folder to any faraday-plugins command:

faraday-plugins list-plugins --custom-plugins-folder /path/to/my/plugins/

3 — Plugin File Structure

Create a folder for your plugin inside the custom plugins directory. The folder name must match the pattern [a-zA-Z0-9_-]+ (letters, digits, hyphens, underscores).

my_awesome_tool/
    __init__.py          # Leave this file empty
    plugin.py            # Your plugin implementation

Warning

Both file names must be exactly __init__.py and plugin.py.


4 — Plugin Class Hierarchy

PluginBase                         ← base for command-only plugins
└── PluginByExtension              ← matches files by extension
    ├── PluginXMLFormat            ← XML reports  (.xml)
    ├── PluginJsonFormat           ← JSON reports (.json)
    ├── PluginMultiLineJsonFormat  ← JSONL / multi-line JSON (.json)
    ├── PluginCSVFormat            ← CSV reports  (.csv)
    └── PluginZipFormat            ← ZIP archives (.zip)

All classes live in faraday_plugins.plugins.plugin.


5 — The createPlugin Factory Function

Every plugin.py must export a createPlugin function. This is how the plugin manager discovers and instantiates your plugin:

def createPlugin(*args, **kwargs):
    return MyToolPlugin(*args, **kwargs)

The manager passes keyword arguments such as ignore_info, hostname_resolution, vuln_tag, host_tag, service_tag, min_severity, and max_severity. Using *args, **kwargs ensures forward-compatibility.


6 — File / Report Plugins

Each subclass of PluginByExtension provides a different strategy for detecting whether a given file belongs to your plugin.

6.1 — PluginXMLFormat

Matches files by XML root tag (and optionally, root tag attributes).

Attribute Type Purpose
identifier_tag str or list[str] XML root element name(s) to match
identifier_tag_attributes set Attribute key-value pairs the root element must have
extension str or list File extension (default ".xml")
from faraday_plugins.plugins.plugin import PluginXMLFormat
import xml.etree.ElementTree as ET


class ExampleToolXmlParser:
    """Separate parser class keeps parsing logic clean."""

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

    def _parse(self, xml_output):
        vulns = []
        tree = ET.fromstring(xml_output)
        for item in tree.iterfind("details/item"):
            vulns.append({
                "ip": item.get("ip"),
                "os": item.get("os", "unknown"),
                "name": item.find("issue").text,
                "severity": item.find("issue").get("severity", "info"),
            })
        return vulns


class ExampleToolPlugin(PluginXMLFormat):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.identifier_tag = "example_tool"
        self.id = "example_tool"
        self.name = "Example Tool"
        self.plugin_version = "1.0.0"

    def parseOutputString(self, output):
        parser = ExampleToolXmlParser(output)
        for v in parser.vulns:
            h_id = self.createAndAddHost(v["ip"], os=v["os"])
            self.createAndAddVulnToHost(h_id, v["name"], severity=v["severity"])


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

Non-standard extensions — If the tool produces XML with a custom extension (e.g., .nessus), set self.extension = ".nessus".

6.2 — PluginJsonFormat

Matches JSON files by checking that a set of keys exists at the top level.

Attribute Type Purpose
json_keys set Required top-level keys (checked as a subset)
filter_keys set If any of these keys are present, the file is rejected
extension str or list File extension (default ".json")
from faraday_plugins.plugins.plugin import PluginJsonFormat
import json


class MyJsonPlugin(PluginJsonFormat):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.id = "mytool_json"
        self.name = "My Tool (JSON)"
        self.plugin_version = "1.0.0"
        self.json_keys = {"results", "scan_info"}
        # Optional: reject files that also have a key from another tool
        self.filter_keys = {"nuclei_version"}

    def parseOutputString(self, output):
        data = json.loads(output)
        for result in data.get("results", []):
            h_id = self.createAndAddHost(result["ip"])
            self.createAndAddVulnToHost(
                h_id,
                result["title"],
                desc=result.get("description", ""),
                severity=result.get("severity", "info"),
                cve=result.get("cves", []),
            )


def createPlugin(*args, **kwargs):
    return MyJsonPlugin(*args, **kwargs)

6.3 — PluginMultiLineJsonFormat

For tools that produce one JSON object per line (JSONL / newline-delimited JSON). The json_keys set is checked against every line.

from faraday_plugins.plugins.plugin import PluginMultiLineJsonFormat
import json


class MyJsonlPlugin(PluginMultiLineJsonFormat):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.id = "mytool_jsonl"
        self.name = "My Tool (JSONL)"
        self.plugin_version = "1.0.0"
        self.json_keys = {"template-id", "host", "info"}

    def parseOutputString(self, output):
        for line in output.splitlines():
            if not line.strip():
                continue
            entry = json.loads(line)
            h_id = self.createAndAddHost(entry["host"])
            self.createAndAddVulnToHost(
                h_id,
                entry["info"].get("name", "Unknown"),
                severity=entry["info"].get("severity", "info"),
            )


def createPlugin(*args, **kwargs):
    return MyJsonlPlugin(*args, **kwargs)

6.4 — PluginCSVFormat

Matches CSV files by column header names.

Attribute Type Purpose
csv_headers set or list[set] Required CSV header names (subset check). A list of sets matches if any set matches.
extension str or list File extension (default ".csv")
from faraday_plugins.plugins.plugin import PluginCSVFormat
import csv
import io


class MyCsvPlugin(PluginCSVFormat):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.id = "mytool_csv"
        self.name = "My Tool (CSV)"
        self.plugin_version = "1.0.0"
        self.csv_headers = {"ip", "port", "vulnerability", "severity"}

    def parseOutputString(self, output):
        reader = csv.DictReader(io.StringIO(output))
        for row in reader:
            h_id = self.createAndAddHost(row["ip"])
            s_id = self.createAndAddServiceToHost(
                h_id, row.get("service", "unknown"),
                ports=[int(row["port"])]
            )
            self.createAndAddVulnToService(
                h_id, s_id,
                row["vulnerability"],
                severity=row.get("severity", "info"),
            )


def createPlugin(*args, **kwargs):
    return MyCsvPlugin(*args, **kwargs)

6.5 — PluginZipFormat

Matches ZIP archives by checking which files they contain.

Attribute Type Purpose
files_list set Expected file names inside the ZIP (intersection check)
extension str or list File extension (default ".zip")
from faraday_plugins.plugins.plugin import PluginZipFormat
import json


class MyZipPlugin(PluginZipFormat):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.id = "mytool_zip"
        self.name = "My Tool (ZIP)"
        self.plugin_version = "1.0.0"
        self.files_list = {"scan_results.json", "metadata.json"}

    def parseOutputString(self, output):
        # `output` is a ZipFile object for PluginZipFormat
        with output.open("scan_results.json") as f:
            data = json.loads(f.read())
        for vuln in data.get("vulnerabilities", []):
            h_id = self.createAndAddHost(vuln["host"])
            self.createAndAddVulnToHost(h_id, vuln["name"], severity=vuln["severity"])


def createPlugin(*args, **kwargs):
    return MyZipPlugin(*args, **kwargs)

Note

For PluginZipFormat, the parseOutputString method receives a zipfile.ZipFile object (not a string).


7 — Command Plugins

Command plugins intercept shell commands by matching against a regex pattern. Set the _command_regex attribute to detect commands your plugin can handle.

If the plugin is command-only, inherit from PluginBase. If it also supports report files, inherit from the appropriate file plugin class (e.g., PluginXMLFormat) and add _command_regex.

7.1 — Basic Command Plugin

import re
from faraday_plugins.plugins.plugin import PluginBase


class CmdPingPlugin(PluginBase):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **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 createPlugin(*args, **kwargs):
    return CmdPingPlugin(*args, **kwargs)

When a user runs ping -c4 192.168.1.1 in Faraday, the plugin manager:

  1. Matches the command against _command_regex.
  2. Calls processCommandString() (which you can override to modify the command).
  3. Executes the command.
  4. Passes stdout to parseOutputString().

7.2 — Commands That Produce Output Files

Some tools write results to files rather than stdout (e.g., Nmap's -oX). For these, set _use_temp_file = True and _temp_file_extension, then override processCommandString to inject the output path into the command:

import re
from faraday_plugins.plugins.plugin import PluginXMLFormat


class NmapPlugin(PluginXMLFormat):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.identifier_tag = "nmaprun"
        self.id = "Nmap"
        self.name = "Nmap XML Output Plugin"
        self.plugin_version = "0.0.3"
        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]+).*$")

    def processCommandString(self, username, current_path, command_string):
        """Inject -oX parameter if the user didn't provide one."""
        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,
            )

    def parseOutputString(self, output):
        # Parse the XML report here...
        pass


def createPlugin(*args, **kwargs):
    return NmapPlugin(*args, **kwargs)

When _use_temp_file is set:

  • The plugin creates a temp file and stores its path in self._output_file_path.
  • After execution, the file contents (not stdout) are passed to parseOutputString().
  • The temp file is automatically cleaned up.

8 — Data Model API

The PluginBase class provides methods to create hosts, services, and vulnerabilities. Each create* method returns an ID you need when creating child objects.

8.1 — createAndAddHost

def createAndAddHost(
    self,
    name,                   # IP address (required)
    os="unknown",           # Operating system
    hostnames=None,         # List of hostnames
    mac=None,               # MAC address
    description="",         # Description text
    tags=None,              # List of tag strings
) -> host_id

8.2 — createAndAddServiceToHost

def createAndAddServiceToHost(
    self,
    host_id,                # ID from createAndAddHost()
    name,                   # Service name (e.g., "http", "ssh")
    protocol="tcp",         # Protocol: "tcp", "udp", etc.
    ports=None,             # Port number (int, str, or list — first element used)
    status="open",          # "open", "closed", or "filtered"
    version="",             # Service version string
    description="",         # Description text
    tags=None,              # List of tag strings
) -> service_id

8.3 — createAndAddVulnToHost

def createAndAddVulnToHost(
    self,
    host_id,                # ID from createAndAddHost()
    name,                   # Vulnerability name (required)
    desc="",                # Description
    ref=None,               # List of reference strings or dicts
    severity="",            # "info", "low", "med", "high", "critical"
    resolution="",          # Remediation guidance
    data="",                # Technical details / additional data
    external_id=None,       # External identifier (e.g., plugin-specific ID)
    run_date=None,          # datetime object (converted to UTC timestamp)
    impact=None,            # Impact dict
    custom_fields=None,     # Dict of custom field values
    status="",              # "open" or "closed" (default: "open")
    policyviolations=None,  # List of policy violation strings
    easeofresolution=None,  # Ease-of-resolution string
    confirmed=False,        # Whether the vulnerability is confirmed
    tags=None,              # List of tag strings
    cve=None,               # List of CVE IDs (e.g., ["CVE-2021-44228"])
    cwe=None,               # List of CWE IDs (e.g., ["CWE-79"])
    cvss2=None,             # CVSS v2 score dict
    cvss3=None,             # CVSS v3 score dict
    cvss4=None,             # CVSS v4 score dict
) -> vuln_id

8.4 — createAndAddVulnToService

def createAndAddVulnToService(
    self,
    host_id,                # ID from createAndAddHost()
    service_id,             # ID from createAndAddServiceToHost()
    name,                   # Vulnerability name (required)
    desc="",                # Description
    ref=None,               # List of reference strings or dicts
    severity="",            # "info", "low", "med", "high", "critical"
    resolution="",          # Remediation guidance
    data="",                # Technical details
    external_id=None,       # External identifier
    run_date=None,          # datetime object
    custom_fields=None,     # Dict of custom field values
    policyviolations=None,  # List of policy violation strings
    impact=None,            # Impact dict
    status="",              # "open" or "closed" (default: "open")
    confirmed=False,        # Whether confirmed
    easeofresolution=None,  # Ease-of-resolution string
    tags=None,              # List of tag strings
    cve=None,               # List of CVE IDs
    cwe=None,               # List of CWE IDs
    cvss2=None,             # CVSS v2 score dict
    cvss3=None,             # CVSS v3 score dict
    cvss4=None,             # CVSS v4 score dict
) -> vuln_id

8.5 — createAndAddVulnWebToService

For web-application vulnerabilities that include HTTP request/response context:

def createAndAddVulnWebToService(
    self,
    host_id,                # ID from createAndAddHost()
    service_id,             # ID from createAndAddServiceToHost()
    name,                   # Vulnerability name (required)
    desc="",                # Description
    ref=None,               # References
    severity="",            # Severity level
    resolution="",          # Remediation
    website="",             # Base URL
    path="",                # URL path
    request="",             # Full HTTP request
    response="",            # HTTP response body
    method="",              # HTTP method (GET, POST, …)
    pname="",               # Vulnerable parameter name
    params="",              # Parameters string
    query="",               # Query string
    category="",            # Vulnerability category (e.g., OWASP)
    data="",                # Technical details
    external_id=None,       # External identifier
    confirmed=False,        # Whether confirmed
    status="",              # "open" or "closed"
    easeofresolution=None,  # Ease-of-resolution
    impact=None,            # Impact dict
    policyviolations=None,  # Policy violations
    status_code=None,       # HTTP status code (int)
    custom_fields=None,     # Custom fields dict
    run_date=None,          # datetime object
    tags=None,              # Tags
    cve=None,               # CVE IDs
    cvss2=None,             # CVSS v2 dict
    cvss3=None,             # CVSS v3 dict
    cvss4=None,             # CVSS v4 dict
    cwe=None,               # CWE IDs
) -> vuln_id

8.6 — createAndAddCredToService

def createAndAddCredToService(
    self,
    host_id,                # ID from createAndAddHost()
    service_id,             # ID from createAndAddServiceToHost()
    username,               # Username string
    password,               # Password string
) -> credential_id

8.7 — Severity Normalization

Severity values are automatically normalized. You can pass:

  • Strings: "info", "low", "med" / "medium", "high", "critical" (case-insensitive, prefix-matched)
  • Numeric strings: "0" → info, "1" → low, "2" → med, "3" → high, "4" → critical
  • Anything else → "unclassified"

8.8 — CVE / CWE Validation

CVE and CWE identifiers are validated before storage:

  • CVE format: CVE-YYYY-NNNNN (4+ digit ID, validated by regex)
  • CWE format: CWE-NNN (2+ digit ID, validated by regex)

Invalid entries are silently filtered out.


9 — Report Detection Mechanism

When Faraday receives a report file, it identifies the correct plugin using this strategy (in order):

9.1 — By Filename Convention

If the filename matches *_faraday_{plugin_id}.* (e.g., scan_faraday_nmap.xml), the plugin is loaded directly by ID.

9.2 — By File Content

The plugin manager attempts to parse the file as:

  1. XML → extracts root tag and attributes → passes to report_belongs_to(main_tag=..., main_tag_attributes=...)
  2. JSON → extracts top-level keys → passes to report_belongs_to(file_json_keys=...)
  3. CSV → extracts column headers → passes to report_belongs_to(file_csv_headers=...)
  4. ZIP → lists contained files → passes to report_belongs_to(files_in_zip=...)

The first plugin whose report_belongs_to() returns True is selected.


10 — Real-World Plugin Examples

10.1 — Command Plugin: Ping

A minimal command plugin that creates a host entry when a ping succeeds.

Source: faraday_plugins/plugins/repo/ping/plugin.py

import re
from faraday_plugins.plugins.plugin import PluginBase


class CmdPingPlugin(PluginBase):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **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 createPlugin(*args, **kwargs):
    return CmdPingPlugin(*args, **kwargs)

10.2 — Report Plugin: Dirsearch (JSON)

A JSON report plugin that groups discovered endpoints by host and HTTP status code. Demonstrates PluginJsonFormat with json_keys and the createAndAddVulnToHost method.

Source: faraday_plugins/plugins/repo/dirsearchjson/plugin.py (added 2025)

import re
import json
from math import floor
from faraday_plugins.plugins.plugin import PluginJsonFormat


class DirsearchPluginJSON(PluginJsonFormat):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.id = "dirsearchjson"
        self.name = "dirsearch Plugin for JSON output"
        self.plugin_version = "0.4.3"
        self.version = "1.0.0"
        self.json_keys = {"results", "info"}
        self._temp_file_extension = "json"

    def parseOutputString(self, output):
        json_report = json.loads(output)
        if not json_report:
            return

        data_regroup = {}
        regex_map = {}

        for result in json_report.get("results", []):
            loc = result.get("url")
            status = result.get("status")
            if loc is None or status is None:
                continue
            status_round = floor(status / 100) * 100
            if status_round == 400:
                continue

            # Extract hostname from URL
            if loc not in regex_map:
                regex_res = re.findall(
                    r"((?:http|https)://(\S*?)(?::[0-9]*|)(?:/|$))", loc
                )
                if regex_res and regex_res[0][1]:
                    regex_map[loc] = regex_res[0][1]
                if regex_map.get(loc) not in data_regroup:
                    data_regroup[regex_map[loc]] = {}

            host = regex_map.get(loc)
            if host and status_round not in data_regroup[host]:
                data_regroup[host][status_round] = (
                    f"One or more endpoints returned "
                    f"**{int(status_round / 100)}xx** :\n"
                )
            if host:
                data_regroup[host][status_round] += f"- [{status}] **[{loc}]({loc})**\n"

        for host in data_regroup:
            h = self.createAndAddHost(host, hostnames=[host])
            for code in data_regroup[host]:
                self.createAndAddVulnToHost(
                    h,
                    f"Returned {int(code / 100)}xx",
                    desc="Exposed files detected in the application.",
                    data=data_regroup[host][code],
                    severity="info",
                    confirmed=True,
                )


def createPlugin(*args, **kwargs):
    return DirsearchPluginJSON(*args, **kwargs)

10.3 — Advanced Plugin: Nuclei (Multi-Line JSON with Version Detection)

The Nuclei plugin demonstrates several advanced patterns:

  • PluginMultiLineJsonFormat for JSONL output
  • Strategy pattern — abstract parser with version-specific implementations (v2.x vs v3.x)
  • processCommandString to inject output flags into the command
  • CVE/CWE/CVSS mapping from tool-specific fields

Source: faraday_plugins/plugins/repo/nuclei/plugin.py

# Simplified excerpt showing the architecture pattern
from abc import ABC, abstractmethod
from faraday_plugins.plugins.plugin import PluginMultiLineJsonFormat


class NucleiReportParser(ABC):
    """Abstract base for version-specific parsers."""
    def __init__(self, vuln_dict):
        self.vuln_dict = vuln_dict
        self.info = vuln_dict.get("info", {})

    @abstractmethod
    def get_impact(self) -> str: ...

    @abstractmethod
    def get_resolution(self) -> str: ...

    @staticmethod
    @abstractmethod
    def can_parse(vuln_dict) -> bool: ...


class NucleiV3Parser(NucleiReportParser):
    def get_impact(self):
        return self.info.get("impact", "")

    def get_resolution(self):
        return self.info.get("remediation", "")

    @staticmethod
    def can_parse(vuln_dict):
        info = vuln_dict.get("info", {})
        return "impact" in info or "remediation" in info


class NucleiV2Parser(NucleiReportParser):
    def get_impact(self):
        return self.info.get("metadata", {}).get("impact", "")

    def get_resolution(self):
        return self.info.get("metadata", {}).get("resolution", "")

    @staticmethod
    def can_parse(vuln_dict):
        return True  # fallback parser


class NucleiPlugin(PluginMultiLineJsonFormat):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.id = "nuclei"
        self.name = "Nuclei"
        self.json_keys = {"template-id", "host", "info"}

    def _get_parser(self, vuln_dict):
        """Select the correct parser based on report format."""
        for parser_class in [NucleiV3Parser, NucleiV2Parser]:
            if parser_class.can_parse(vuln_dict):
                return parser_class(vuln_dict)

    def parseOutputString(self, output):
        for line in output.splitlines():
            if not line.strip():
                continue
            vuln_dict = json.loads(line)
            parser = self._get_parser(vuln_dict)
            # ... create hosts, services, vulnerabilities using parser data

11 — Testing and Debugging

11.1 — CLI Commands

The faraday-plugins CLI provides five commands for testing:

Command Purpose
list-plugins List all loaded plugins and their capabilities
detect-command "<cmd>" Identify which plugin matches a command
process-command "<cmd>" Execute and parse a command
detect-report <file> Identify which plugin matches a report file
process-report <file> Parse a report file and output JSON

Common options (available on most commands):

Flag Description
-cpf / --custom-plugins-folder PATH Load custom plugins from this directory
--plugin_id ID Force a specific plugin (skip auto-detection)
-o / --output-file PATH Write JSON output to a file
--summary Show summary counts instead of full JSON
--ignore-info Skip informational-severity vulnerabilities
--min-severity LEVEL Minimum severity to include
--max-severity LEVEL Maximum severity to include
--vuln-tag TAG Add a tag to all vulnerabilities
--host-tag TAG Add a tag to all hosts
--service-tag TAG Add a tag to all services

11.2 — Step-by-Step Testing

1. Verify your plugin loads:

faraday-plugins list-plugins -cpf /path/to/my/plugins/

Available Plugins:
...
example_tool - Example Tool
Loaded Plugins: 119

2. Test report detection:

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

Plugin: example_tool

3. Test report processing:

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

This outputs the JSON data structure that Faraday would import — verify the hosts, services, and vulnerabilities match your expectations.

4. Force a specific plugin:

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

5. Test command detection (for command plugins):

faraday-plugins detect-command "nmap -sV 192.168.1.1"

Plugin: Nmap

11.3 — Debug Mode

Enable verbose debug logging with the PLUGIN_DEBUG environment variable:

export PLUGIN_DEBUG=1
faraday-plugins process-report /path/to/report.xml

This outputs detailed logs including:

  • Plugin loading sequence
  • Report detection matching attempts
  • File extension, XML tag, JSON key, CSV header matches
  • Parse errors and warnings

Log format:

YYYY-MM-DD HH:MM:SS,sss - logger.name - LEVEL [filename.py:line - funcName()]  Message

11.4 — Plugin Logger

Every plugin inherits a self.logger instance. Use it for debug output in your plugin:

def parseOutputString(self, output):
    self.logger.debug("Starting parse, output length: %d", len(output))
    # ...
    self.logger.warning("Skipping malformed entry: %s", entry)

11.5 — Writing Automated Tests

The faraday-plugins repository uses pytest for testing. Here's the standard pattern:

import json
import pytest
from faraday_plugins.plugins.manager import PluginsManager, ReportAnalyzer

plugins_manager = PluginsManager()
analyzer = ReportAnalyzer(plugins_manager)


def test_report_detection():
    """Verify the report is detected by the correct plugin."""
    plugin = analyzer.get_plugin("path/to/test_report.xml")
    assert plugin is not None
    assert plugin.id == "example_tool"


def test_report_parsing():
    """Verify the report is parsed correctly."""
    plugin = plugins_manager.get_plugin("example_tool")
    plugin.processReport("path/to/test_report.xml")
    data = plugin.get_data()

    assert len(data["hosts"]) == 3
    assert data["hosts"][0]["ip"] == "10.23.49.232"
    assert len(data["hosts"][0]["services"][0]["vulnerabilities"]) == 1


def test_command_detection():
    """Verify command string is matched by the plugin."""
    from faraday_plugins.plugins.manager import CommandAnalyzer
    cmd_analyzer = CommandAnalyzer(plugins_manager)
    plugin = cmd_analyzer.get_plugin("ping -c4 192.168.1.1")
    assert plugin is not None
    assert plugin.id == "ping"

Tip: Place sample report files in a test_data/ folder alongside your tests.


12 — Submitting Your Plugin

Contributing to the Official Repository

  1. Fork the faraday-plugins repository.

  2. Create your plugin under faraday_plugins/plugins/repo/your_tool/:

    faraday_plugins/plugins/repo/your_tool/
        __init__.py
        plugin.py
    

  3. Add test data — place sample report files in the report collection used by the test suite.

  4. Add command detection tests — if your plugin handles commands, add entries to tests/commands.json:

    {
        "plugin_id": "your_tool",
        "command": "yourtool --scan target",
        "command_result": "yourtool --scan target"
    }
    

  5. Run the test suite:

    pytest tests/
    

  6. Submit a pull request to the infobyte/faraday_plugins GitHub repository. Including a sample report file in the PR is highly recommended, as it is needed to add test coverage for the new plugin.

Using as a Custom Plugin

If you only need the plugin for your own Faraday instance:

  1. Place the plugin folder in your custom plugins directory (typically ~/.faraday/plugins/).
  2. Ensure the custom_plugins_folder setting is configured in Faraday.
  3. Restart Faraday (or use the CLI with --custom-plugins-folder).

13 — Quick Reference

Plugin Checklist

  • [ ] plugin.py contains a createPlugin(*args, **kwargs) function
  • [ ] Plugin class has a unique self.id
  • [ ] self.name is set to a human-readable name
  • [ ] self.plugin_version is set
  • [ ] parseOutputString() is implemented
  • [ ] For file plugins: correct base class and identifier attributes are set
  • [ ] For command plugins: _command_regex is defined
  • [ ] faraday-plugins list-plugins shows the plugin
  • [ ] faraday-plugins detect-report or detect-command identifies it correctly
  • [ ] faraday-plugins process-report or process-command produces valid JSON

Common Patterns

Pattern When to use
Separate parser class Complex XML/JSON parsing (keeps parseOutputString clean)
Strategy / version detection Tool has multiple output format versions
_use_temp_file + processCommandString Tool writes output to a file instead of stdout
filter_keys on PluginJsonFormat Avoid matching reports from a different tool with overlapping JSON keys
identifier_tag_attributes on PluginXMLFormat Root XML tag is too generic (e.g., <report>)
Multiple json_keys sets (list of sets) Tool has multiple valid JSON output schemas
Multiple csv_headers sets (list of sets) Tool has multiple valid CSV output formats

Severity Mapping

Input Normalized
"info", "informational", "0" info
"low", "1" low
"med", "medium", "2" med
"high", "3" high
"critical", "cri", "4" critical
Anything else unclassified