Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Development

Adding a new plugin

Plugins are the unit of work in iottack. Each plugin performs one focused test and reports zero or more findings with a severity level and optional evidence string. Adding a plugin requires no changes to main.rs or any central registry. The framework discovers plugins automatically at startup.

A complete working reference lives at src/protocols/example/example.rs. The walkthrough below follows that file step by step.

1. Create the module files

Plugins are grouped by protocol family under src/protocols/. Create a directory for the protocol if one does not exist yet, then add the plugin source file and the module declaration:

src/protocols/<protocol>/
├── mod.rs          ← declares pub mod <plugin>;
└── <plugin>.rs     ← the plugin implementation

Expose the new protocol module from src/protocols/mod.rs:

#![allow(unused)]
fn main() {
pub mod <protocol>;
}

2. Define the plugin struct

The struct holds every parameter the plugin needs between initialize and execute. Fields should mirror the CLI arguments declared in meta().

#![allow(unused)]
fn main() {
pub struct ExamplePlugin {
    message: String,
    fail: bool,
}

impl ExamplePlugin {
    pub fn new() -> Self {
        Self { message: String::new(), fail: false }
    }
}

impl Default for ExamplePlugin {
    fn default() -> Self { Self::new() }
}
}

3. Implement Plugin::meta

meta() returns a PluginMeta built with the plugin_meta! macro. The macro derives the plugin name and category from the Rust module path automatically.

Module pathDerived name
iottack::protocols::http::bannerhttp/banner
iottack::protocols::network::tcp_bannernetwork/tcp-banner
iottack::protocols::example::exampleexample/example

The args slice describes every CLI flag. main.rs reads this slice to build the clap subcommand dynamically.

#![allow(unused)]
fn main() {
fn meta(&self) -> PluginMeta {
    plugin_meta!(
        summary: "Example plugin, shows the full plugin authoring pattern",
        description: "A reference plugin that exercises every part of the Plugin \
                      API: initialize, execute, teardown, CLI arguments, findings \
                      at multiple severity levels, and the inventory registration \
                      macro.",
        needs_root: false,
        args: &[
            CliArg {
                name: "message",        // config key; becomes --message on the CLI
                short: Some('m'),
                help: "Text to include in the Info finding",
                value_name: "TEXT",
                default: Some("hello from example plugin"),
                is_flag: false,
            },
            CliArg {
                name: "fail",
                short: Some('x'),
                help: "Emit a High finding and exit with status Fail",
                value_name: "",
                default: None,
                is_flag: true,          // presence sets config key to "true"
            },
        ],
    )
}
}

4. Implement Plugin::initialize

initialize is called once before execute. Read every CLI argument from config and store the values on self.

MethodWhen to use
config.get_or(key, default)String value with a fallback
config.parse_or(key, default)Typed value (u16, u64, …) with a fallback
config.require(key)Value that must be present — returns Err if missing
#![allow(unused)]
fn main() {
fn initialize(&mut self, config: &PluginConfig) -> Result<()> {
    self.message = config.get_or("message", "hello from example plugin");
    self.fail    = config.get("fail") == Some("true");
    Ok(())
}
}

5. Implement Plugin::execute

execute contains the core test logic. Create a PluginResult, add PluginFinding values, and call result.finish(status) before returning.

Severity levels (lowest to highest): Info -> Low -> Medium -> High -> Critical

Use PluginFinding::with_evidence to attach the raw data that triggered a finding: a banner, a header value, a captured packet and so on. Evidence is shown in the text output and preserved in JSON results.

#![allow(unused)]
fn main() {
fn execute(&mut self) -> Result<PluginResult> {
    let mut result = PluginResult::new(self.meta().name);

    // Neutral observation — always emitted.
    result.add(PluginFinding::new(
        Severity::Info,
        format!("Example plugin executed: {}", self.message),
        "Informational finding describing an observed property of the target.",
    ));

    // Conditional finding with evidence.
    if self.fail {
        result.add(
            PluginFinding::new(
                Severity::High,
                "Simulated failure",
                "The --fail flag was set.",
            )
            .with_evidence("flag: --fail was present"),
        );
        result.finish(PluginStatus::Fail);
        return Ok(result);
    }

    result.finish(PluginStatus::Pass);
    Ok(result)
}
}

Early-return with PluginStatus::Fail for expected failure conditions such as a refused connection or a missing required argument. Reserve Err(...) / ? for truly unexpected failures.

6. Implement Plugin::teardown (optional)

teardown is always called after execute, even when execute returned an error. Use it to close sockets, file handles or serial ports. The default implementation is a no-op. Please omit this method if there is nothing to release.

#![allow(unused)]
fn main() {
fn teardown(&mut self) -> Result<()> {
    Ok(())
}
}

7. Register the plugin

Add a single line at the bottom of the file. The framework collects all registered factories at startup via the inventory crate. Thus, no changes to main.rs or any registry file are needed.

#![allow(unused)]
fn main() {
inventory::submit! {
    crate::PluginFactory(|| Box::new(ExamplePlugin::new()) as Box<dyn crate::Plugin>)
}
}

8. Add documentation

Create a Markdown file at documentation/src/plugins/<protocol>/<plugin-name>.md and add it to documentation/src/SUMMARY.md. See http - banner for an example of the expected structure: one-liner, usage block, description, checks table with severities, options table and example commands.

Running the example plugin

# Pass status with default message
iottack example example

# Custom message
iottack example example --message "testing 1 2 3"

# Trigger the High finding and Fail status
iottack example example --fail

# Save JSON output
iottack example example --fail --output result.json --format json

Running the tests

cargo test

The test suite covers the core types (PluginConfig, PluginFinding, PluginResult, Severity, Category, PluginRegistry) and the output-formatting helpers in main.rs. Doc-tests embedded in the public API are run as part of the same cargo test invocation.