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 .py file 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:

python main.py --list-plugins

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

def on_scan_completed(engine=None):
    requests.post(URL, json=dict(engine.metrics), timeout=10)

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>/toggle enable/disable (no restart needed)
  • A plugin may provide a webui HTML entry, embedded in the console via iframe
  • With a schema, a JSON-Schema form is rendered automatically and config is stored in output/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.