This guide outlines the development workflow for adding new plugins or editing existing plugins in SecuScan. Following these steps ensures your plugin changes load correctly, pass schema validations, and satisfy automated tests.
SecuScan features a pluggable architecture. Plugins are self-contained scanner descriptions that define:
- Metadata (
metadata.json): Schema fields, safety configuration, run engine types, and checksums. - Parser (
parser.py): (Optional) Python code to normalize raw CLI/Docker output into the structured findings format.
To maintain system integrity, the backend verifies plugin files before loading them. Any modifications to a plugin must be accompanied by a refreshed integrity checksum and valid schema properties.
Each plugin is located in its own subdirectory under plugins/. A standard plugin directory has the following structure:
plugins/
my_plugin/
metadata.json # Required: contains schema and CLI/Docker engine run arguments
parser.py # Required if output.parser is "custom": parses stdout to JSON
A minimal, valid metadata.json has the following shape:
{
"id": "my_plugin",
"name": "My Plugin",
"version": "1.0.0",
"description": "What this plugin does.",
"category": "recon",
"icon": "🔍",
"engine": {
"type": "cli",
"binary": "mytool"
},
"command_template": ["mytool", "{target}"],
"fields": [
{
"id": "target",
"label": "Target Host",
"type": "string",
"required": true
}
],
"output": {
"parser": "text"
},
"safety": {
"level": "safe",
"requires_consent": false
},
"checksum": "d131dd02c5e6eec4693d9a0698bc92ee2c2d8471ff0f8ef612c6a0c4e7c99b82"
}Important
The checksum field in metadata.json secures the integrity of the plugin definition. You should never manually edit this field. It is calculated and populated using helper scripts.
For detailed documentation on field types, options, validation keys, and custom pattern presets, see docs/plugin-validation.md.
If you need to update a plugin's settings, arguments, or parsing behavior, use the following linear workflow:
Edit plugins/<plugin_id>/metadata.json or plugins/<plugin_id>/parser.py as needed.
Any change to metadata.json or parser.py invalidates the plugin's checksum. The backend will reject the plugin on startup if its checksum does not match its contents, causing unrelated backend tests to fail.
Run the refresh helper to calculate and save the new checksum:
python scripts/refresh_plugin_checksum.py --plugin <plugin_id>Ensure that your changes follow the metadata rules and schema definitions:
python scripts/validate_plugin.py --plugin <plugin_id>Verify everything is working correctly by running the backend pytest suite:
# Run all tests
./testing/test_python.sh
# Or target plugin tests specifically for fast feedback:
python -m pytest testing/backend/unit/test_plugin_integrity.py
python -m pytest testing/backend/integration/test_parser_output_contract.pyEnsure you commit both the source modifications and the updated metadata.json (containing the new checksum) in the same commit.
The checksum utility scripts/refresh_plugin_checksum.py provides several arguments for managing plugin integrity:
- Refresh a single plugin:
python scripts/refresh_plugin_checksum.py --plugin <plugin-id>
- Refresh all plugins at once:
python scripts/refresh_plugin_checksum.py --all
- Preview updates (Dry Run):
Check which plugins are out of date without changing any files:
If dry run reports
python scripts/refresh_plugin_checksum.py --all --dry-run
[UPDATE]for any plugin, you must run the script without--dry-runbefore committing.
Follow this procedure when contributing a new scanner or tool integration:
Create a new folder under plugins/ with a unique, lowercase snake_case ID (e.g. plugins/my_scanner/). Inside, add:
metadata.jsonparser.py(if usingoutput.parser = "custom")
If your plugin parses tool outputs into structured findings, set "parser": "custom" under "output" in metadata.json.
Your parser.py must export a callable parse(output: str) -> dict function that implements the parser contract:
- It accepts a single
strargument containing the raw standard output of the tool. - It returns a dictionary.
- The returned dictionary must contain the following keys:
count(int): The number of findings detected.findings(listofdict): Individual finding descriptions. Each finding dictionary can optionally definetitle,description,severity(e.g.,info,low,medium,high,critical), andmetadata.summary(listofstr): A list of text lines summarizing the scan outcomes.
If a plugin output is raw text and does not require structured dissection, set "parser": "text" instead. The backend handles pass-through parsing automatically and no parser.py is needed.
To generate the first checksum, add a dummy "checksum": "" string in your metadata.json and execute:
python scripts/refresh_plugin_checksum.py --plugin <your_plugin_id>Add your new plugin details to the index in PLUGINS.md. Then validate and test it.
Parser unit tests in SecuScan are hermetic and decoupled from the actual binaries. Instead of invoking CLI commands or launching Docker containers, parser tests pass pre-recorded string outputs (fixtures) straight into the parse() function.
This design makes testing extremely fast, reproducible, and runnable in a basic CI/CD runner without installing any security scanners or tool binaries.
When adding or updating a custom parser, write a corresponding unit test file: testing/backend/unit/test_<plugin_id>_parser.py.
Follow the inline-fixture pattern used in test_dns_enum_parser.py:
- Import the parse function:
from plugins.my_plugin.parser import parse
- Define raw tool output: Create a multiline string simulating actual tool output.
- Execute the parser: Call
result = parse(raw_output). - Assert details: Assert on the dictionary keys, expected counts, and finding content.
- Handle edge cases: Write a test asserting that empty input (
"") or malformed/garbage input doesn't crash the parser.
Under testing/backend/unit/fixtures/plugins/, you will find:
valid_plugin/: Contains a schema-perfectmetadata.jsondemonstrating required structures. Used by validator tests to prove valid layouts pass.invalid_plugin/: Deliberately violates rules (invalid safety level, missing name, duplicate field IDs, custom parser without a file). Used to assert validator error reporting.
Note
Do not modify these core fixtures unless you are explicitly changing the parser validation or schema rules in the backend validator.
When you submit a PR, the CI runner verifies plugin health across three primary test suites:
| Test File | Verification Goal | Reason for Failure |
|---|---|---|
| test_plugin_integrity.py | Checks that checksums match and IDs/names are globally unique. | Outdated checksum, forgotten checksum generation, or duplicated plugin metadata ID/Name. |
| test_plugin_validator.py | Asserts the validation rules correctly validate metadata rules against fixtures. | A regression in validation logic (rarely triggered by simple plugin edits). |
| test_parser_output_contract.py | Verifies custom parser imports and output compliance on empty and raw structures. | A custom parser does not export a callable parse() function, crashes on load, or returns a dictionary missing mandatory output keys. |
Prior to committing and opening a Pull Request for a plugin change, run through this checklist:
-
metadata.jsondeclares all required fields. - Any custom
parser.pyexportsparse(output: str) -> dict. - Checksum has been updated with
python scripts/refresh_plugin_checksum.py --plugin <id>. - Local validator exits cleanly:
python scripts/validate_plugin.py --plugin <id>. - A dedicated parser test file exists under
testing/backend/unit/test_<id>_parser.pyand covers happy/unhappy paths. - All python backend tests pass:
./testing/test_python.sh. - The plugin index in PLUGINS.md has been updated (if adding a new plugin).