Modules
EXPERIMENTAL
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.
Overview
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 (setup.py)
- 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.
Warning
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 ghcr.io/aquasecurity/trivy-module-spring4shell
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 ghcr.io/aquasecurity/trivy-test-images:spring4shell-jre8
2022-06-12T12:57:13.210+0300 INFO Loading ghcr.io/aquasecurity/trivy-module-spring4shell/spring4shell.wasm...
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+ │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-22965 │
├──────────────────────────────────────────────────────────────┼─────────────────────┼──────────┼───────────────────┼────────────────────────┼────────────────────────────────────────────────────────────┤
...(snip)...
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 ghcr.io/aquasecurity/trivy-module-spring4shell
Building Modules
It supports TinyGo only at the moment.
TinyGo
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.
Tips
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 github.com/aquasecurity/trivy-module-wordpress
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
}
Info
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{
`wp-includes\/version.php`,
}
}
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=") {
continue
}
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
}
Tips
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 {
continue
}
for _, c := range result.CustomResources {
if c.Type != typeWPVersion {
continue
}
wpVersion = c.Data.(string)
wasm.Info(fmt.Sprintf("WordPress Version: %s", wpVersion))
...snip...
if affectedVersion.Check(ver) {
vulnerable = true
}
break
}
}
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.
Build
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 wordpress.wasm ~/.trivy/modules
Distribute Your Module
You can distribute your own module in OCI registries. Please follow the oras installation instruction.
oras push ghcr.io/aquasecurity/trivy-module-wordpress:latest wordpress.wasm:application/vnd.module.wasm.content.layer.v1+wasm
Uploading 3daa3dac086b wordpress.wasm
Pushed ghcr.io/aquasecurity/trivy-module-wordpress:latest
Digest: sha256:6416d0199d66ce52ced19f01d75454b22692ff3aa7737e45f7a189880840424f