Z-Sans ships with a built-in event-driven plugin framework. Drop any .py file into the plugins/ directory and it is loaded automatically; the engine dispatches events to it throughout the scan lifecycle. You can use it to implement custom reports, external threat intelligence (e.g. Shodan), vulnerability scanning, Webhook notifications, audit logging, and more.
Mechanism Overview¶
- A plugin is a Python file or directory: a
.pyfile is a plugin; a directory must contain an entry file (<dirname>.py/plugin.py/main.py/__init__.py, looked up in that order) - Detection rules: every module-level object whose name starts with
on_and is callable is automatically registered as a handler for the corresponding event; other functions are not registered and can serve as plain utility functions - Isolation: exceptions raised during handling are caught and logged and do not interrupt the scan
- CLI integration (optional): a plugin may implement
register_cli(parser)to add its own command-line arguments; these arguments exist only while the plugin is loaded - Zero configuration: just drop it into the directory and it works — no changes to the main program required
Write a Plugin in 5 Minutes¶
Create hello.py under plugins/:
VERSION = "0.1.0"
DESCRIPTION = "Logs every new asset discovery"
ALL_ASSETS = 0
FOUND_BEFORE = set()
def on_asset_discovered(asset, source=None):
"""Called every time a new asset is added to the store"""
global ALL_ASSETS
ALL_ASSETS += 1
uid = asset.uid if hasattr(asset, "uid") else str(asset)
print(f"[hello] NEW: {uid} (from {source})")
def on_scan_completed(engine=None):
"""Called when the scan completes"""
print(f"[hello] Scan done, discovered {ALL_ASSETS} new assets total")
Run --list-plugins to inspect:
Hook detection
Define at least one on_* handler; if none is defined the plugin still loads, but a warning is logged.
Plugin Manifest __manifest__¶
A plugin may declare metadata via the __plugin__ or __manifest__ dict:
__manifest__ = {
"name": "hello",
"version": "0.1.0",
"description": "Logs every new asset discovery",
"author": "you@example.com",
}
| Field | Description | Default |
|---|---|---|
name |
Unique identifier, used as the plugin key | File name |
version |
Version number | "0.0.0" |
description |
Description | empty |
author |
Author | empty |
webui |
Web entry file (relative to the plugin directory) | Auto-detects webui.html etc. |
schema |
JSON-Schema style configuration form | none |
conflicts / conflict_with / incompatible |
Mutual-exclusion declarations (supports * ? fnmatch wildcards) |
none |
Legacy constant compatibility: PLUGIN_NAME / PLUGIN_VERSION / PLUGIN_DESCRIPTION / PLUGIN_AUTHOR / PLUGIN_WEBUI / PLUGIN_SCHEMA.
Platform Capabilities: What You Get¶
Handlers can access the platform object via the engine argument:
| Object/attribute | Description |
|---|---|
engine.config |
Fully deep-merged configuration |
engine.asset_graph |
The asset graph (.nodes / .edges / add_asset() / add_edge()), thread-safe |
engine.queue |
The priority queue (.add() / .size() / get_next()) |
engine.metrics |
assets_processed / new_assets_found / depth_reached / errors |
engine.output_handler.run_dir |
Output directory of the current scan (put plugin artifacts here) |
engine.save_checkpoint() / load_checkpoint() |
Checkpoint save/load |
engine.register_hook / engine.emit_hook |
Event bus |
engine.plugins |
Dict of loaded plugins |
engine.plugin_conflicts |
Conflict records |
Asset object fields: uid (type:value), type, value, depth, state, properties (dict), to_dict().
Concurrency safety
Asset processing runs concurrently in ThreadPoolExecutor(concurrency.max_tasks), so on_asset_* may be invoked concurrently. Lock any shared state yourself, and handlers must not raise.
Common Patterns¶
Custom Report (standalone file)¶
def on_scan_completed(engine=None):
run_dir = engine.output_handler.run_dir
with open(os.path.join(run_dir, "summary.txt"), "w") as f:
f.write(f"total assets: {len(engine.asset_graph.nodes)}\n")
External Intelligence (Shodan)¶
def on_asset_discovered(asset, source=None):
if asset.type == "ip":
# Call your threat intelligence API, write to run_dir or properties
asset.properties["shodan_tags"] = query_shodan(asset.value)
Webhook Notifications¶
CLI Integration¶
def register_cli(parser):
parser.add_argument("--hello-to", default="world", help="Say hello to whom")
When multiple plugins register the same argument name, the first registered wins and the conflict is recorded in engine.plugin_conflicts, visible under the CONFLICTS: section of --list-plugins.
Plugin Management¶
Command Line¶
python main.py --list-plugins # Table: NAME/VERSION/HANDLERS/KIND/STATUS/EVENTS
python main.py --plugin-info hello # Details: author/description/path/file listing/docs
Configuration Switches¶
plugins:
dir: null # Plugin directory; null uses plugins/ under the project root
disabled: [] # Disabled list, by plugin name
List replacement
plugins.disabled is a whole-list replacement. If a user config overrides the built-in default list, the default entries are lost. The Web console rewrites using text-level regex and does not remove comments.
Web Console¶
POST /api/plugins/<name>/toggleenable/disable (no restart needed)- A plugin may provide a
webuiHTML entry, embedded in the console via iframe - With a
schema, a JSON-Schema form is rendered automatically and config is stored inoutput/plugin_config/<name>.yaml
Plugin Conflicts¶
| Conflict type (reason) | Scenario |
|---|---|
duplicate_source |
Same name from two sources (foo.py + foo/), the first in sort order wins |
declared_name_conflict |
Two plugins declare the same name in their __manifest__ |
mutual_conflict |
A conflicts mutual-exclusion declaration is hit |
cli_option_conflict |
Multiple plugins register the same CLI argument name |
Secrets and Security Recommendations¶
- Plugin code runs in your environment; only install plugins from trusted sources
- Do not hardcode secrets in plugins; read them from environment variables or
output/plugin_config/(plugin config) instead - Plugins that send data externally should mind data compliance for the destination
Appendix: Event Quick Reference¶
| Event | Firing time |
|---|---|
on_scan_started |
Scan starts, output directory already created |
on_scan_completed |
Scan completes successfully, before the report is exported |
on_scan_stopped |
Stop / abnormal cleanup |
on_asset_scanned |
A single asset finished breeding |
on_asset_discovered |
A new asset entered the store |
on_asset_excluded |
An asset was excluded |
on_asset_eliminated |
An asset was eliminated |
on_asset_failed |
Asset processing failed |
See the Event Reference for detailed payloads.