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 Go 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.
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:
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.
Version() returns your module version and should be incremented after updates.
Name() returns your module name.
packagemainconst(version=1name="wordpress-module")// main is required for Go to compile the Wasm modulefuncmain(){}typeWordpressModulestruct{// Cannot define fields as modules can't keep state.}func(WordpressModule)Version()int{returnversion}func(WordpressModule)Name()string{returnname}
Info
A struct cannot have any fields. Each method invocation is performed in different states.
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().
consttypeWPVersion="wordpress-version"func(WordpressModule)RequiredFiles()[]string{return[]string{`wp-includes\/version.php`,}}func(WordpressModule)Analyze(filePathstring)(*serialize.AnalysisResult,error){f,err:=os.Open(filePath)// e.g. filePath: /usr/src/wordpress/wp-includes/version.phpiferr!=nil{returnnil,err}deferf.Close()varwpVersionstringscanner:=bufio.NewScanner(f)forscanner.Scan(){line:=scanner.Text()if!strings.HasPrefix(line,"$wp_version="){continue}ss:=strings.Split(line,"=")iflen(ss)!=2{returnnil,fmt.Errorf("invalid wordpress version: %s",line)}// NOTE: it is an example; you actually need to handle comments, etcss[1]=strings.TrimSpace(ss[1])wpVersion=strings.Trim(ss[1],`";`)}iferr=scanner.Err();err!=nil{returnnil,err}return&serialize.AnalysisResult{CustomResources:[]ftypes.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.
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{returnserialize.PostScanSpec{Action:api.ActionInsert,// Add new vulnerabilities}}func(WordpressModule)PostScan(resultstypes.Results)(types.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"// }// ]// }// ] varwpVersionintfor_,result:=rangeresults{ifresult.Class!=types.ClassCustom{continue}for_,c:=rangeresult.CustomResources{ifc.Type!=typeWPVersion{continue}wpVersion=c.Data.(string)wasm.Info(fmt.Sprintf("WordPress Version: %s",wpVersion))...snip...ifaffectedVersion.Check(ver){vulnerable=true}break}}ifvulnerable{// Add CVE-2020-36326results=append(results,types.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",},},},})}returnresults,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.