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:
- Matches the command against
_command_regex. - Calls
processCommandString()(which you can override to modify the command). - Executes the command.
- 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:
- XML → extracts root tag and attributes → passes to
report_belongs_to(main_tag=..., main_tag_attributes=...) - JSON → extracts top-level keys → passes to
report_belongs_to(file_json_keys=...) - CSV → extracts column headers → passes to
report_belongs_to(file_csv_headers=...) - 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:
PluginMultiLineJsonFormatfor JSONL output- Strategy pattern — abstract parser with version-specific implementations (v2.x vs v3.x)
processCommandStringto 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¶
-
Fork the faraday-plugins repository.
-
Create your plugin under
faraday_plugins/plugins/repo/your_tool/:faraday_plugins/plugins/repo/your_tool/ __init__.py plugin.py -
Add test data — place sample report files in the report collection used by the test suite.
-
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" } -
Run the test suite:
pytest tests/ -
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:
- Place the plugin folder in your custom plugins directory (typically
~/.faraday/plugins/). - Ensure the
custom_plugins_foldersetting is configured in Faraday. - Restart Faraday (or use the CLI with
--custom-plugins-folder).
13 — Quick Reference¶
Plugin Checklist¶
- [ ]
plugin.pycontains acreatePlugin(*args, **kwargs)function - [ ] Plugin class has a unique
self.id - [ ]
self.nameis set to a human-readable name - [ ]
self.plugin_versionis set - [ ]
parseOutputString()is implemented - [ ] For file plugins: correct base class and identifier attributes are set
- [ ] For command plugins:
_command_regexis defined - [ ]
faraday-plugins list-pluginsshows the plugin - [ ]
faraday-plugins detect-reportordetect-commandidentifies it correctly - [ ]
faraday-plugins process-reportorprocess-commandproduces 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 |