Trivy Module System vs Plugins: Understanding Wasm Extensions and Binary Plugins
Trivy modules are sandboxed WebAssembly extensions that run inside the Trivy CLI to customize scanning logic, while plugins are standalone external binaries executed as sub-commands with full host access.
The Trivy module system is an experimental extensibility mechanism in the aquasecurity/trivy repository that allows developers to write WebAssembly (Wasm) extensions. These modules integrate directly into the scanning pipeline without requiring recompilation of the core binary, offering a lighter alternative to the traditional plugin architecture.
What Is the Trivy Module System?
Trivy’s module system enables Wasm-based extensions that execute inside the Trivy process using the wazero runtime. This approach eliminates CGO dependencies while providing near-native performance through zero-copy memory access.
Wasm Runtime and Discovery
At startup, Trivy walks the directory configured by --module-dir (default $HOME/.trivy/modules) and loads every *.wasm file it finds. In pkg/module/module.go, the loadModules function handles this discovery process, compiling each binary with wazero and validating compatibility before instantiation.
API Contract and Entry Points
Each module must expose an api_version function that matches the Trivy SDK version (tapi.Version). If version numbers differ, the module is silently ignored to prevent runtime errors.
A module implements one or both extension points defined in the wasmModule struct:
analyze(filePath string, data []byte) -> json– Implements custom file analysis logicpost_scan(results []Result) -> json– Mutates scan results after the core scan completes
After instantiation, the Register() method adds the module to Trivy’s analyzer registry or post-scan hook system.
Security Model and State Management
Modules run in a sandboxed environment with no direct file-system, network, or OS access unless explicitly exposed through the Trivy SDK. The architecture enforces stateless execution—modules cannot persist mutable data across calls, ensuring consistent behavior during concurrent scans.
Installation occurs via trivy module install, which pulls Wasm binaries as OCI artifacts (e.g., ghcr.io/aquasecurity/trivy-module-spring4shell) into the modules directory.
What Are Trivy Plugins?
Trivy plugins are standalone executables that extend the CLI through external process invocation. Unlike modules, plugins operate outside Trivy’s memory space and communicate via standard input/output streams.
Binary Execution Model
Located in pkg/plugin/plugin.go, the plugin system uses exec.Command to spawn separate processes when users invoke trivy <plugin-name> (e.g., trivy kubectl). The Cmd, Run, and Start functions manage plugin lifecycle, executing binaries discovered in $HOME/.trivy/plugins or the XDG data directory.
Plugins run without sandboxing, inheriting the same privileges as the parent Trivy process. This allows unrestricted access to the host environment, including the ability to spawn additional processes or modify system state.
Interface and Distribution
Plugins implement the Trivy plugin protocol by reading JSON from stdin and writing results to stdout. No specific SDK is required—any language capable of producing an executable and parsing JSON can implement a plugin.
Distribution formats include OCI images, Git repositories, or plain archives. The plugin.yaml manifest describes metadata and installation requirements. Unlike modules, plugins support lazy loading—binaries execute only when their specific sub-command is invoked.
Key Differences Between Modules and Plugins
The architectural distinction centers on isolation versus flexibility:
| Feature | Trivy Modules (Wasm) | Trivy Plugins (Binary) |
|---|---|---|
| Runtime | In-process via wazero | Separate OS process |
| Isolation | Sandboxed VM with restricted system access | Full host access, no sandbox |
| Performance | Zero-copy memory, no IPC overhead | Process creation and JSON serialization overhead |
| State | Stateless (enforced by SDK) | Can maintain state across invocations |
| Compatibility | Version enforced via api_version function |
Manual version management by author |
| Distribution | OCI artifact with *.wasm layer |
OCI, zip, tarball, or plain binary |
| Loading | Pre-loaded at startup (all enabled modules) | Lazy-loaded on sub-command invocation |
| Scope | File analyzers and post-scan result mutations | CLI sub-commands, output formats, custom reporters |
When to Use Modules vs Plugins
Choose the extension mechanism based on your security requirements and integration depth:
Prefer Modules when:
- You need sandboxed execution for untrusted third-party code
- You want to extend scanning logic (custom file analyzers or result post-processing)
- You require minimal runtime overhead for high-throughput scanning
- You need users to selectively enable extensions via
--enable-modules
Prefer Plugins when:
- You are adding a new CLI sub-command (e.g.,
trivy kubectl) - You need to reuse existing binary tools without rewriting in Go
- You require stateful operations or external network/file access
- You want to implement custom output formats as standalone processors
Implementation Examples
Creating a Wasm Module
The following example implements a minimal analyzer that processes .txt files:
package main
import (
"github.com/aquasecurity/trivy/pkg/module/wasm"
"github.com/aquasecurity/trivy/pkg/module/serialize"
)
const version = 1
const name = "example-analyzer"
var required = []string{`.*\.txt$`}
//export analyze
func analyze(ptrSize uint64) uint64 {
var res serialize.AnalysisResult
// Analysis logic here
return marshalResult(res)
}
//export required
func requiredFiles() uint64 {
return marshalJSON(required)
}
//export name
func moduleName() uint64 {
return marshalString(name)
}
//export version
func moduleVersion() int32 {
return int32(version)
}
//export api_version
func apiVersion() int32 {
// Must match Trivy's SDK version (see pkg/module/api)
return int32(1)
}
type Module struct{}
func (m Module) Name() string { return name }
func (m Module) Version() int { return version }
func init() {
wasm.RegisterModule(Module{})
}
func main() {}
Compile the module using:
GOOS=wasip1 GOARCH=wasm go build -o mod.wasm -buildmode=c-shared
Install and run:
trivy module install ghcr.io/aquasecurity/trivy-module-example
trivy fs ./my-project
Creating a Plugin
This plugin example reads Trivy JSON from stdin and injects a custom vulnerability:
package main
import (
"encoding/json"
"io"
"log"
"os"
"github.com/aquasecurity/trivy/pkg/types"
)
func main() {
var in types.Results
if err := json.NewDecoder(os.Stdin).Decode(&in); err != nil && err != io.EOF {
log.Fatalf("decode error: %v", err)
}
for i := range in {
in[i].Vulnerabilities = append(in[i].Vulnerabilities, types.DetectedVulnerability{
VulnerabilityID: "CUSTOM-001",
PkgName: "example",
Severity: "MEDIUM",
Title: "Example custom finding",
})
}
if err := json.NewEncoder(os.Stdout).Encode(in); err != nil {
log.Fatalf("encode error: %v", err)
}
}
Install and use as an output plugin:
go build -o trivy-example-plugin .
trivy plugin install ./trivy-example-plugin
trivy image alpine:latest --format json --output plugin=example-plugin
Summary
- Trivy modules are Wasm-based extensions running in
pkg/module/module.gothat provide sandboxed, in-process analysis and post-scan hooks with enforced statelessness. - Trivy plugins are external binaries managed by
pkg/plugin/plugin.gothat execute as sub-commands with full host privileges but higher overhead. - Modules use the wazero runtime for zero-copy performance; plugins use
exec.Commandfor flexibility. - Choose modules for security-sensitive scan extensions; choose plugins for CLI tooling and system integrations.
Frequently Asked Questions
Are Trivy modules production-ready?
No, the Trivy module system is currently experimental according to the source code in pkg/module/module.go. While functional, the API surface may change between versions, and the api_version strictness (requiring exact SDK version matching) means modules must be updated to match newer Trivy releases.
Can I use languages other than Go for Trivy modules?
While the Wasm standard supports any language, Trivy currently only officially provides a Go SDK (pkg/module/api, pkg/module/serialize). To write modules in other languages (Rust, C++, etc.), you would need to manually implement the ABI contract that the wasmModule struct expects, including memory management conventions and JSON serialization formats.
How do I debug a Wasm module during development?
Since modules run inside wazero, standard debugging tools are limited. The Trivy SDK exposes logging helpers (logDebug, logInfo, etc.) in pkg/module/module.go that write to Trivy’s log output. Enable debug logging with --debug to see module lifecycle messages and analysis traces during execution.
Can plugins modify scan results like modules can?
Plugins can modify results only when used as output plugins (--output plugin=<name>), receiving the complete JSON result set via stdin and returning modified JSON via stdout. However, they cannot intervene during the scan phase like modules can. Modules integrate at the analyze and post_scan hooks defined in the analyzer and extension registries, allowing real-time result mutation during the scanning process.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →