Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.malbox.app/llms.txt

Use this file to discover all available pages before exploring further.

Getting started

Create a new repository from the C++ plugin template by clicking Use this template on GitHub. Clone your new repository and rename the project in CMakeLists.txt and plugin.toml to match your plugin name. The template gives you a working plugin with CMake configuration, a plugin.toml manifest, example source code, and CI already set up.

Prerequisites

  • C++20 compiler (GCC 11+, Clang 13+, or MSVC 2019 16.10+)
  • CMake 3.20+
  • For Windows guest plugins: MinGW cross-compilation toolchain (provided by the Malbox devenv)

Project structure

Your plugin project has three essential files: a CMake build file, a plugin manifest, and your source code.

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(my-plugin LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(MalboxPluginSDK REQUIRED)

# Generate the compile-time runtime-config header from plugin.toml.
malbox_generate_runtime_config(
    MANIFEST ${CMAKE_CURRENT_SOURCE_DIR}/plugin.toml
    OUTPUT   ${CMAKE_CURRENT_BINARY_DIR}/generated/malbox_runtime_config.hpp
)

add_executable(my-plugin src/main.cpp ${MALBOX_GENERATED_HEADERS})
target_include_directories(my-plugin PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/generated)
target_link_libraries(my-plugin PRIVATE MalboxPluginSDK::MalboxPluginSDK)
The malbox_generate_runtime_config function invokes the malbox-codegen CLI (shipped with the SDK) to read your plugin.toml and emit a malbox_runtime_config.hpp header with constexpr values. The header is regenerated whenever plugin.toml changes.

plugin.toml

Every plugin needs a manifest alongside its binary. This tells the daemon how to manage your plugin.
[plugin]
name = "my-plugin"
version = "0.1.0"
description = "My analysis plugin"
authors = ["Your Name"]
type = "guest"                    # or "host"

[runtime]
state = "ephemeral"               # or "persistent", "scoped"
execution = "exclusive"           # or "sequential", "parallel", "unrestricted"
port = 50051                      # gRPC listen port (default: 50051)
log_filter = "info"               # tracing filter (default: "info")

[runtime.paths]
sample_dir       = "C:\\malbox\\samples"      # where daemon pushes the sample
artifact_dir     = "C:\\malbox\\artifacts"    # where plugin writes output (auto-collected)
stash_dir        = "C:\\malbox\\stash"        # internal result stash spillover
log_dir          = "C:\\malbox\\logs"         # SDK-internal log overflow files
external_log_dir = "C:\\malbox\\ext-logs"     # external tool/driver logs (auto-collected)

[runtime.stash]
threshold_bytes = 1048576
ttl_secs = 120

[results.my_result]
description = "What this result contains"
user_visible = true
display_name = "My Result"
render = "json"
See Plugin Configuration for details on plugin types, state management, and execution contexts.

src/main.cpp

#include <malbox/plugin.hpp>
#include "malbox_runtime_config.hpp"   // generated from plugin.toml

class MyPlugin final : public malbox::Plugin {
public:
    void on_start(const std::unordered_map<std::string, std::string>& config) override {
        // Called once before tasks arrive. Use config for initialization.
    }

    void on_task(const malbox::Task& task, const malbox::Context& ctx) override {
        auto sample = task.sample_bytes();
        ctx.emit_progress(0.5, "analyzing");

        // ... perform analysis ...

        std::string json = R"({"status": "clean"})";
        auto data = std::span<const uint8_t>{
            reinterpret_cast<const uint8_t*>(json.data()),
            json.size()
        };
        ctx.push_result(malbox::PluginResult::json("my_result", data));
    }

    void on_stop() override {
        // Called once on shutdown. Clean up resources.
    }
};

int main() {
    malbox::PluginMeta meta{};
    meta.name        = "my-plugin";
    meta.version     = "0.1.0";
    meta.description = "My analysis plugin";
    meta.authors     = "Your Name";
    meta.plugin_type = malbox::PluginType::Guest;
    meta.state       = malbox::PluginState::Ephemeral;
    meta.execution   = malbox::ExecutionContext::Exclusive;

    malbox::run_guest_plugin(
        std::make_unique<MyPlugin>(),
        meta,
        malbox::generated::runtime_config);
}
Your plugin subclasses malbox::Plugin and implements on_task at minimum. The on_start and on_stop lifecycle hooks are optional. For a host plugin, change the metadata and entry point:
meta.plugin_type = malbox::PluginType::Host;
malbox::run_host_plugin(std::make_unique<MyPlugin>(), meta);

Handler methods

Override virtual methods on the malbox::Plugin base class:
MethodSignatureRequired
on_task(const Task&, const Context&) -> voidYes
on_start(const std::unordered_map<std::string, std::string>&) -> voidNo
on_stop() -> voidNo
health_check() -> HealthStatusNo
on_daemon_event(DaemonEvent, const Context&) -> voidNo
on_task_event(TaskEvent, const TaskEventPayload&, const Context&) -> voidNo
on_plugin_event(PluginEvent, const PluginEventPayload&, const Context&) -> voidNo
on_sample_event(SampleEvent, const SampleEventPayload&, const Context&) -> voidNo
Only on_task is required (pure virtual). All other handlers default to no-ops.

Pushing results

Results are pushed to the daemon during on_task via ctx.push_result(). You can call it multiple times to stream results incrementally. There are three result types:
// JSON-encoded result (pass raw UTF-8 bytes)
ctx.push_result(PluginResult::json("name", byte_span));

// Raw binary result
ctx.push_result(PluginResult::bytes("name", byte_span));

// Reference a file on disk (runtime reads it)
ctx.push_result(PluginResult::file("name", "/path/to/file"));
Each result name should match an entry in your plugin.toml under [results.*].

Reports

For structured analysis output with verdicts, indicators, TTPs, and frontend-renderable sections, use the ReportBuilder to construct a Report and push it as a result:
#include <malbox/report.hpp>

using namespace malbox::report;

auto report = ReportBuilder("my-plugin", "0.1.0")
    .summary("Analysis complete")
    .verdict(Classification::Malicious, 85, Confidence::High)
    .indicator(Indicator("sha256", "abc123..."))
    .section("overview", "Overview", [](SectionBuilder& s) {
        s.markdown("Found malicious indicators");
    })
    .build();

ctx.push_result(into_plugin_result(report));
See the C++ SDK Reference for the full report API.

Handling errors

Plugin methods can throw malbox::Error to signal failures. The SDK catches exceptions at the FFI boundary and reports them to the runtime.
#include <malbox/error.hpp>

// In your on_task:
if (something_wrong) {
    throw malbox::Error(-1, "analysis failed: corrupt sample");
}
SDK methods like task.sample_bytes() also throw malbox::Error on failure.

Subscribing to events

Beyond task processing, your plugin can react to system events by overriding the event handler methods:
class MyPlugin final : public malbox::Plugin {
public:
    void on_task(const malbox::Task& task, const malbox::Context& ctx) override {
        // ...
    }

    void on_task_event(
        TaskEvent event,
        const TaskEventPayload& payload,
        const Context& ctx
    ) override {
        // React to task lifecycle events
    }

    void on_sample_event(
        SampleEvent event,
        const SampleEventPayload& payload,
        const Context& ctx
    ) override {
        // React to sample processing events
    }
};
See the Events Reference for the full list of available events.

Thread safety

When using ExecutionContext::Parallel, multiple on_task calls may execute concurrently. Protect shared mutable state with std::mutex or similar synchronization primitives. Health checks may also arrive on a different thread regardless of execution context.

Build

Linux

Host plugins always target Linux (they run on the daemon machine). Guest plugins targeting a Linux guest VM also use this:
cmake -B build -DCMAKE_PREFIX_PATH=/path/to/malbox-plugin-sdk/dist
cmake --build build

Windows (cross-compile)

Guest plugins that run inside a Windows analysis VM need to be cross-compiled. The Malbox devenv provides a MinGW toolchain and a CMake toolchain file for this:
cmake -B build-win \
  -DCMAKE_TOOLCHAIN_FILE=/path/to/malbox-plugin-sdk/cmake/mingw-w64-x86_64.cmake \
  -DCMAKE_PREFIX_PATH=/path/to/malbox-plugin-sdk/dist-windows
cmake --build build-win
This produces a statically linked .exe binary with no runtime DLL dependencies.
The MinGW toolchain file reads MINGW_LIB_PATH and MINGW_INCLUDE_PATH environment variables for library and header search paths. The Malbox devenv sets these automatically.

Deploy

Host plugins

Place the compiled binary and plugin.toml in a subdirectory of the daemon’s plugin directory. The daemon discovers plugins automatically on startup.
~/.config/malbox/plugins/
my-host-plugin-bin
plugin.toml

Guest plugins

Guest plugin binaries must be deployed inside the analysis VM. Place them in the daemon’s plugin directory (so the daemon knows about them), then provision the binary into the VM:
~/.config/malbox/plugins/
my-guest-plugin-bin
plugin.toml
Provision into the VM using the deploy playbook:
cargo run -p malbox-cli machine provision <machine_id> \
  --provisioner ansible \
  --config '{"playbook": "configuration/infrastructure/ansible/playbooks/windows/deploy-guest-plugin.yml"}' \
  --snapshot <snapshot_name> \
  --plugins my-guest-plugin

Examples

See the example plugins on GitHub.