Skip to content



This feature might change without preserving backwards compatibility.

Trivy provides a module feature to allow others to extend the Trivy CLI without the need to change the Trivy code base. It changes the behavior during scanning by WebAssembly.


Trivy modules are add-on tools that integrate seamlessly with Trivy. They provide a way to extend the core feature set of Trivy, but without updating the Trivy binary.

  • They can be added and removed from a Trivy installation without impacting the core Trivy tool.
  • They can be written in any programming language supporting WebAssembly.
  • It supports only TinyGo at the moment.

You can write your own detection logic.

  • Evaluate complex vulnerability conditions like Spring4Shell
  • Detect a shell script communicating with malicious domains
  • Detect malicious python install script (
  • Even detect misconfigurations in WordPress setting
  • etc.

Then, you can update the scan result however you want.

  • Change a severity
  • Remove a vulnerability
  • Add a new vulnerability
  • etc.

Modules should be distributed in OCI registries like GitHub Container Registry.


WebAssembly doesn't allow file access and network access by default. Modules can read required files only, but cannot overwrite them. WebAssembly is sandboxed and secure by design, but Trivy modules available in public are not audited for security. You should install and run third-party modules at your own risk even though

Under the hood Trivy leverages wazero to run WebAssembly modules without CGO.

Installing a Module

A module can be installed using the trivy module install command. This command takes an url. It will download the module and install it in the module cache.

Trivy adheres to the XDG specification, so the location depends on whether XDG_DATA_HOME is set. Trivy will now search XDG_DATA_HOME for the location of the Trivy modules cache. The preference order is as follows:

  • XDG_DATA_HOME if set and .trivy/plugins exists within the XDG_DATA_HOME dir
  • $HOME/.trivy/plugins

For example, to download the WebAssembly module, you can execute the following command:

$ trivy module install

Using Modules

Once the module is installed, Trivy will load all available modules in the cache on the start of the next Trivy execution. The modules may inject custom logic into scanning and change the result. You can run Trivy as usual and modules are loaded automatically.

You will see the log messages about WASM modules.

$ trivy image
2022-06-12T12:57:13.210+0300    INFO    Loading
2022-06-12T12:57:13.596+0300    INFO    Registering WASM module: spring4shell@v1
2022-06-12T12:57:14.865+0300    INFO    Module spring4shell: Java Version: 8, Tomcat Version: 8.5.77
2022-06-12T12:57:14.865+0300    INFO    Module spring4shell: change CVE-2022-22965 severity from CRITICAL to LOW

Java (jar)

Total: 9 (UNKNOWN: 1, LOW: 3, MEDIUM: 2, HIGH: 3, CRITICAL: 0)

│                           Library                            │    Vulnerability    │ Severity │ Installed Version │     Fixed Version      │                           Title                            │
│ org.springframework.boot:spring-boot (helloworld.war)        │ CVE-2022-22965      │ LOW      │ 2.6.3             │ 2.5.12, 2.6.6          │ spring-framework: RCE via Data Binding on JDK 9+           │
│                                                              │                     │          │                   │                        │                 │

In the above example, the Spring4Shell module changed the severity from CRITICAL to LOW because the application doesn't satisfy one of conditions.

Uninstalling Modules

Specify a module repository with trivy module uninstall command.

$ trivy module uninstall

Building Modules

It supports TinyGo only at the moment.


Trivy provides Go SDK including three interfaces. Your own module needs to implement either or both Analyzer and PostScanner in addition to Module.

type Module interface {
    Version() int
    Name() string

type Analyzer interface {
    RequiredFiles() []string
    Analyze(filePath string) (*serialize.AnalysisResult, error)

type PostScanner interface {
    PostScanSpec() serialize.PostScanSpec
    PostScan(serialize.Results) (serialize.Results, error)

In the following tutorial, it creates a WordPress module that detects a WordPress version and a critical vulnerability accordingly.


You can use logging functions such as Debug and Info for debugging. See examples for the detail.

Initialize your module

Replace the repository name with yours.

$ go mod init

Module interface

Version() returns your module version and should be incremented after updates. Name() returns your module name.

package main

const (
    version = 1
    name = "wordpress-module"

type WordpressModule struct{
    // Cannot define fields as modules can't keep state.

func (WordpressModule) Version() int {
    return version

func (WordpressModule) Name() string {
    return name


A struct cannot have any fields. Each method invocation is performed in different states.

Analyzer interface

If you implement the Analyzer interface, Analyze method is called when the file path is matched to file patterns returned by RequiredFiles(). A file pattern must be a regular expression. The syntax detail is here.

Analyze takes the matched file path, then the file can be opened by os.Open().

const typeWPVersion = "wordpress-version"

func (WordpressModule) RequiredFiles() []string {
    return []string{

func (WordpressModule) Analyze(filePath string) (*serialize.AnalysisResult, error) {
    f, err := os.Open(filePath) // e.g. filePath: /usr/src/wordpress/wp-includes/version.php
    if err != nil {
        return nil, err
    defer f.Close()

    var wpVersion string
    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        line := scanner.Text()
        if !strings.HasPrefix(line, "$wp_version=") {

        ss := strings.Split(line, "=")
        if len(ss) != 2 {
            return nil, fmt.Errorf("invalid wordpress version: %s", line)

        // NOTE: it is an example; you actually need to handle comments, etc
        ss[1] = strings.TrimSpace(ss[1])
        wpVersion = strings.Trim(ss[1], `";`)

    if err = scanner.Err(); err != nil {
        return nil, err

    return &serialize.AnalysisResult{
        CustomResources: []serialize.CustomResource{
                Type:     typeWPVersion,
                FilePath: filePath,
                Data:     wpVersion,
    }, nil


Trivy caches analysis results according to the module version. We'd recommend cleaning the cache or changing the module version every time you update Analyzer.

PostScanner interface

PostScan is called after scanning and takes the scan result as an argument from Trivy. In post scanning, your module can perform one of three actions:

  • Insert
    • Add a new security finding
    • e.g. Add a new vulnerability and misconfiguration
  • Update
    • Update the detected vulnerability and misconfiguration
    • e.g. Change a severity
  • Delete
    • Delete the detected vulnerability and misconfiguration
    • e.g. Remove Spring4Shell because it is not actually affected.

PostScanSpec() returns which action the module does. If it is Update or Delete, it also needs to return IDs such as CVE-ID and misconfiguration ID, which your module wants to update or delete.

serialize.Results contains the filtered results matching IDs you specified. Also, it includes CustomResources with the values your Analyze returns, so you can modify the scan result according to the custom resources.

func (WordpressModule) PostScanSpec() serialize.PostScanSpec {
    return serialize.PostScanSpec{
        Action: api.ActionInsert, // Add new vulnerabilities

func (WordpressModule) PostScan(results serialize.Results) (serialize.Results, error) {
    // e.g. results
    // [
    //   {
    //     "Target": "",
    //     "Class": "custom",
    //     "CustomResources": [
    //       {
    //         "Type": "wordpress-version",
    //         "FilePath": "/usr/src/wordpress/wp-includes/version.php",
    //         "Layer": {
    //           "DiffID": "sha256:057649e61046e02c975b84557c03c6cca095b8c9accd3bd20eb4e432f7aec887"
    //         },
    //         "Data": "5.7.1"
    //       }
    //     ]
    //   }
    // ]   
    var wpVersion int
    for _, result := range results {
        if result.Class != types.ClassCustom {

        for _, c := range result.CustomResources {
            if c.Type != typeWPVersion {
            wpVersion = c.Data.(string)
            wasm.Info(fmt.Sprintf("WordPress Version: %s", wpVersion))


            if affectedVersion.Check(ver) {
                vulnerable = true

    if vulnerable {
        // Add CVE-2020-36326
        results = append(results, serialize.Result{
            Target: wpPath,
            Class:  types.ClassLangPkg,
            Type:   "wordpress",
            Vulnerabilities: []types.DetectedVulnerability {
                    VulnerabilityID:  "CVE-2020-36326",
                    PkgName:          "wordpress",
                    InstalledVersion: wpVersion,
                    FixedVersion:     "5.7.2",
                    Vulnerability: dbTypes.Vulnerability{
                        Title:    "PHPMailer 6.1.8 through 6.4.0 allows object injection through Phar Deserialization via addAttachment with a UNC pathname.",
                        Severity: "CRITICAL",
    return results, nil

The new vulnerability will be added to the scan results. This example shows how the module inserts a new finding. If you are interested in Update, you can see an example of Spring4Shell.

In the Delete action, PostScan needs to return results you want to delete. If PostScan returns an empty, Trivy will not delete anything.


Follow the install guide and install TinyGo.

$ tinygo build -o wordpress.wasm -scheduler=none -target=wasi --no-debug wordpress.go

Put the built binary to the module directory that is under the home directory by default.

$ mkdir -p ~/.trivy/modules
$ cp spring4shell.wasm ~/.trivy/modules

Distribute Your Module

You can distribute your own module in OCI registries. Please follow the oras installation instruction.

oras push wordpress.wasm:application/vnd.module.wasm.content.layer.v1+wasm
Uploading 3daa3dac086b wordpress.wasm
Digest: sha256:6416d0199d66ce52ced19f01d75454b22692ff3aa7737e45f7a189880840424f