feat: add support for WASM modules (#2195)

This commit is contained in:
Teppei Fukuda
2022-06-15 15:23:00 +03:00
committed by GitHub
parent a02c06bafd
commit 7cecade3a1
86 changed files with 9142 additions and 1320 deletions

View File

@@ -70,7 +70,7 @@ jobs:
uses: goreleaser/goreleaser-action@v3
with:
version: v1.4.1
args: release --rm-dist
args: release --rm-dist --timeout 60m
env:
GITHUB_TOKEN: ${{ secrets.ORG_REPO_TOKEN }}
- name: Checkout trivy-repo

View File

@@ -11,6 +11,7 @@ on:
pull_request:
env:
GO_VERSION: "1.18"
TINYGO_VERSION: "0.23.0"
jobs:
test:
name: Test
@@ -35,6 +36,12 @@ jobs:
with:
version: v1.45
args: --deadline=30m
skip-cache: true # https://github.com/golangci/golangci-lint-action/issues/244#issuecomment-1052197778
- name: Install TinyGo
run: |
wget https://github.com/tinygo-org/tinygo/releases/download/v${TINYGO_VERSION}/tinygo_${TINYGO_VERSION}_amd64.deb
sudo dpkg -i tinygo_${TINYGO_VERSION}_amd64.deb
- name: Run unit tests
run: make test
@@ -55,6 +62,28 @@ jobs:
- name: Run integration tests
run: make test-integration
module-test:
name: Module Integration Test
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
id: go
- name: Install TinyGo
run: |
wget https://github.com/tinygo-org/tinygo/releases/download/v${TINYGO_VERSION}/tinygo_${TINYGO_VERSION}_amd64.deb
sudo dpkg -i tinygo_${TINYGO_VERSION}_amd64.deb
- name: Checkout
uses: actions/checkout@v3
- name: Run module integration tests
run: |
make test-module-integration
build-test:
name: Build Test
runs-on: ubuntu-latest
@@ -83,7 +112,7 @@ jobs:
uses: goreleaser/goreleaser-action@v3
with:
version: v1.4.1
args: release --snapshot --rm-dist --skip-publish
args: release --snapshot --rm-dist --skip-publish --timeout 60m
build-documents:
name: Documentation Test

3
.gitignore vendored
View File

@@ -30,3 +30,6 @@ integration/testdata/fixtures/images
# goreleaser output
dist
# WebAssembly
*.wasm

View File

@@ -49,6 +49,7 @@ run:
- ".*._mock.go$"
- ".*._test.go$"
- "integration/*"
- "examples/*"
issues:
exclude-rules:

View File

@@ -1,15 +1,24 @@
VERSION := $(shell git describe --tags)
LDFLAGS=-ldflags "-s -w -X=main.version=$(VERSION)"
LDFLAGS := -ldflags "-s -w -X=main.version=$(VERSION)"
GOPATH=$(shell go env GOPATH)
GOBIN=$(GOPATH)/bin
GOSRC=$(GOPATH)/src
GOPATH := $(shell go env GOPATH)
GOBIN := $(GOPATH)/bin
GOSRC := $(GOPATH)/src
TEST_MODULE_DIR := pkg/module/testdata
TEST_MODULE_SRCS := $(wildcard $(TEST_MODULE_DIR)/*/*.go)
TEST_MODULES := $(patsubst %.go,%.wasm,$(TEST_MODULE_SRCS))
EXAMPLE_MODULE_DIR := examples/module
EXAMPLE_MODULE_SRCS := $(wildcard $(EXAMPLE_MODULE_DIR)/*/*.go)
EXAMPLE_MODULES := $(patsubst %.go,%.wasm,$(EXAMPLE_MODULE_SRCS))
MKDOCS_IMAGE := aquasec/mkdocs-material:dev
MKDOCS_PORT := 8000
u := $(if $(update),-u)
# Tools
$(GOBIN)/wire:
go install github.com/google/wire/cmd/wire@v0.5.0
@@ -22,9 +31,12 @@ $(GOBIN)/golangci-lint:
$(GOBIN)/labeler:
go install github.com/knqyf263/labeler@latest
$(GOBIN)/easyjson:
go install github.com/mailru/easyjson/...@v0.7.7
.PHONY: wire
wire: $(GOBIN)/wire
wire gen ./pkg/...
wire gen ./pkg/commands/... ./pkg/rpc/...
.PHONY: mock
mock: $(GOBIN)/mockery
@@ -35,19 +47,36 @@ deps:
go get ${u} -d
go mod tidy
.PHONY: generate-test-modules
generate-test-modules: $(TEST_MODULES)
# Compile WASM modules for unit and integration tests
%.wasm:%.go
@if !(type "tinygo" > /dev/null 2>&1); then \
echo "Need to install TinyGo. Follow https://tinygo.org/getting-started/install/"; \
exit 1; \
fi
go generate $<
# Run unit tests
.PHONY: test
test:
test: $(TEST_MODULES)
go test -v -short -coverprofile=coverage.txt -covermode=atomic ./...
integration/testdata/fixtures/images/*.tar.gz: $(GOBIN)/crane
mkdir -p integration/testdata/fixtures/images/
integration/scripts/download-images.sh
# Run integration tests
.PHONY: test-integration
test-integration: integration/testdata/fixtures/images/*.tar.gz
go test -v -tags=integration ./integration/...
# Run WASM integration tests
.PHONY: test-module-integration
test-module-integration: integration/testdata/fixtures/images/*.tar.gz $(EXAMPLE_MODULES)
go test -v -tags=module_integration ./integration/...
.PHONY: lint
lint: $(GOBIN)/golangci-lint
$(GOBIN)/golangci-lint run --timeout 5m
@@ -78,12 +107,18 @@ install:
clean:
rm -rf integration/testdata/fixtures/images
# Create labels on GitHub
.PHONY: label
label: $(GOBIN)/labeler
labeler apply misc/triage/labels.yaml -r aquasecurity/trivy -l 5
# Run MkDocs development server to preview the documentation page
.PHONY: mkdocs-serve
## Runs MkDocs development server to preview the documentation page
mkdocs-serve:
docker build -t $(MKDOCS_IMAGE) -f docs/build/Dockerfile docs/build
docker run --name mkdocs-serve --rm -v $(PWD):/docs -p $(MKDOCS_PORT):8000 $(MKDOCS_IMAGE)
# Generate JSON marshaler/unmarshaler for TinyGo/WebAssembly as TinyGo doesn't support encoding/json.
.PHONY: easyjson
easyjson: $(GOBIN)/easyjson
easyjson pkg/module/serialize/types.go

View File

@@ -0,0 +1,351 @@
# Modules
!!! warning "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][tinygo] at the moment.
You can write your own detection logic.
- Evaluate complex vulnerability conditions like [Spring4Shell][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][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:
```bash
$ 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.
```shell
$ 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.
```bash
$ 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`.
```go
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](#examples) for the detail.
#### Module interface
`Version()` returns your module version and should be incremented after updates.
`Name()` returns your module name.
```go
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][regexp].
`Analyze` takes the matched file path, then the file can be opened by `os.Open()`.
```go
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.
```go
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][trivy-module-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][tinygo-installation] and install TinyGo.
```bash
$ 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.
```bash
$ 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].
```bash
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
```
## Examples
- [Spring4Shell][trivy-module-spring4shell]
- [WordPress][trivy-module-wordpress]
[regexp]: https://github.com/google/re2/wiki/Syntax
[tinygo]: https://tinygo.org/
[spring4shell]: https://blog.aquasec.com/zero-day-rce-vulnerability-spring4shell
[wazero]: https://github.com/tetratelabs/wazero
[trivy-module-spring4shell]: https://github.com/aquasecurity/trivy/tree/main/examples/module/spring4shell
[trivy-module-wordpress]: https://github.com/aquasecurity/trivy/tree/main/examples/module/wordpress
[tinygo-installation]: https://tinygo.org/getting-started/install/
[oras]: https://oras.land/cli/

View File

@@ -1,8 +1,7 @@
# Kubernetes
!!! warning "EXPERIMENTAL"
This feature might change without preserving backwards compatibility.
This feature might change without preserving backwards compatibility.
The Trivy K8s CLI allows you to scan your Kubernetes cluster for Vulnerabilities, Secrets and Misconfigurations. You can either run the CLI locally or integrate it into your CI/CD pipeline. The difference to the Trivy CLI is that the Trivy K8s CLI allows you to scan running workloads directly within your cluster.

View File

@@ -0,0 +1,17 @@
# Module
```bash
NAME:
trivy module - manage modules
USAGE:
trivy module command [command options] [arguments...]
COMMANDS:
install, i install a module
uninstall, u uninstall a module
help, h Shows a list of commands or help for one command
OPTIONS:
--help, -h show help (default: false)
```

View File

@@ -0,0 +1,21 @@
# Plugin
```bash
NAME:
trivy plugin - manage plugins
USAGE:
trivy plugin command [command options] plugin_uri
COMMANDS:
install, i install a plugin
uninstall, u uninstall a plugin
list, l list installed plugin
info information about a plugin
run, r run a plugin on the fly
update update an existing plugin
help, h Shows a list of commands or help for one command
OPTIONS:
--help, -h show help (default: false)
```

View File

@@ -1,173 +0,0 @@
# Plugins
Trivy provides a plugin feature to allow others to extend the Trivy CLI without the need to change the Trivycode base.
This plugin system was inspired by the plugin system used in [kubectl][kubectl], [Helm][helm], and [Conftest][conftest].
## Overview
Trivy plugins are add-on tools that integrate seamlessly with Trivy.
They provide a way to extend the core feature set of Trivy, but without requiring every new feature to be written in Go and added to the core tool.
- They can be added and removed from a Trivy installation without impacting the core Trivy tool.
- They can be written in any programming language.
- They integrate with Trivy, and will show up in Trivy help and subcommands.
!!! warning
Trivy plugins available in public are not audited for security.
You should install and run third-party plugins at your own risk, since they are arbitrary programs running on your machine.
## Installing a Plugin
A plugin can be installed using the `trivy plugin install` command.
This command takes a url and will download the plugin and install it in the plugin 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 plugins cache.
The preference order is as follows:
- XDG_DATA_HOME if set and .trivy/plugins exists within the XDG_DATA_HOME dir
- ~/.trivy/plugins
Under the hood Trivy leverages [go-getter][go-getter] to download plugins.
This means the following protocols are supported for downloading plugins:
- OCI Registries
- Local Files
- Git
- HTTP/HTTPS
- Mercurial
- Amazon S3
- Google Cloud Storage
For example, to download the Kubernetes Trivy plugin you can execute the following command:
```bash
$ trivy plugin install github.com/aquasecurity/trivy-plugin-kubectl
```
## Using Plugins
Once the plugin is installed, Trivy will load all available plugins in the cache on the start of the next Trivy execution.
A plugin will be made in the Trivy CLI based on the plugin name.
To display all plugins, you can list them by `trivy --help`
```bash
$ trivy --help
NAME:
trivy - A simple and comprehensive vulnerability scanner for containers
USAGE:
trivy [global options] command [command options] target
VERSION:
dev
COMMANDS:
image, i scan an image
filesystem, fs scan local filesystem
repository, repo scan remote repository
client, c client mode
server, s server mode
plugin, p manage plugins
kubectl scan kubectl resources
help, h Shows a list of commands or help for one command
```
As shown above, `kubectl` subcommand exists in the `COMMANDS` section.
To call the kubectl plugin and scan existing Kubernetes deployments, you can execute the following command:
```
$ trivy kubectl deployment <deployment-id> -- --ignore-unfixed --severity CRITICAL
```
Internally the kubectl plugin calls the kubectl binary to fetch information about that deployment and passes the using images to Trivy.
You can see the detail [here][trivy-plugin-kubectl].
If you want to omit even the subcommand, you can use `TRIVY_RUN_AS_PLUGIN` environment variable.
```bash
$ TRIVY_RUN_AS_PLUGIN=kubectl trivy job your-job -- --format json
```
## Installing and Running Plugins on the fly
`trivy plugin run` installs a plugin and runs it on the fly.
If the plugin is already present in the cache, the installation is skipped.
```bash
trivy plugin run github.com/aquasecurity/trivy-plugin-kubectl pod your-pod -- --exit-code 1
```
## Uninstalling Plugins
Specify a plugin name with `trivy plugin uninstall` command.
```bash
$ trivy plugin uninstall kubectl
```
## Building Plugins
Each plugin has a top-level directory, and then a plugin.yaml file.
```bash
your-plugin/
|
|- plugin.yaml
|- your-plugin.sh
```
In the example above, the plugin is contained inside of a directory named `your-plugin`.
It has two files: plugin.yaml (required) and an executable script, your-plugin.sh (optional).
The core of a plugin is a simple YAML file named plugin.yaml.
Here is an example YAML of trivy-plugin-kubectl plugin that adds support for Kubernetes scanning.
```yaml
name: "kubectl"
repository: github.com/aquasecurity/trivy-plugin-kubectl
version: "0.1.0"
usage: scan kubectl resources
description: |-
A Trivy plugin that scans the images of a kubernetes resource.
Usage: trivy kubectl TYPE[.VERSION][.GROUP] NAME
platforms:
- selector: # optional
os: darwin
arch: amd64
uri: ./trivy-kubectl # where the execution file is (local file, http, git, etc.)
bin: ./trivy-kubectl # path to the execution file
- selector: # optional
os: linux
arch: amd64
uri: https://github.com/aquasecurity/trivy-plugin-kubectl/releases/download/v0.1.0/trivy-kubectl.tar.gz
bin: ./trivy-kubectl
```
The `plugin.yaml` field should contain the following information:
- name: The name of the plugin. This also determines how the plugin will be made available in the Trivy CLI. For example, if the plugin is named kubectl, you can call the plugin with `trivy kubectl`. (required)
- version: The version of the plugin. (required)
- usage: A short usage description. (required)
- description: A long description of the plugin. This is where you could provide a helpful documentation of your plugin. (required)
- platforms: (required)
- selector: The OS/Architecture specific variations of a execution file. (optional)
- os: OS information based on GOOS (linux, darwin, etc.) (optional)
- arch: The architecture information based on GOARCH (amd64, arm64, etc.) (optional)
- uri: Where the executable file is. Relative path from the root directory of the plugin or remote URL such as HTTP and S3. (required)
- bin: Which file to call when the plugin is executed. Relative path from the root directory of the plugin. (required)
The following rules will apply in deciding which platform to select:
- If both `os` and `arch` under `selector` match the current platform, search will stop and the platform will be used.
- If `selector` is not present, the platform will be used.
- If `os` matches and there is no more specific `arch` match, the platform will be used.
- If no `platform` match is found, Trivy will exit with an error.
After determining platform, Trivy will download the execution file from `uri` and store it in the plugin cache.
When the plugin is called via Trivy CLI, `bin` command will be executed.
The plugin is responsible for handling flags and arguments. Any arguments are passed to the plugin from the `trivy` command.
## Example
https://github.com/aquasecurity/trivy-plugin-kubectl
[kubectl]: https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/
[helm]: https://helm.sh/docs/topics/plugins/
[conftest]: https://www.conftest.dev/plugins/
[go-getter]: https://github.com/hashicorp/go-getter
[trivy-plugin-kubectl]: https://github.com/aquasecurity/trivy-plugin-kubectl

View File

@@ -0,0 +1,40 @@
# Spring4Shell module
This module provides a more in-depth investigation of Spring4Shell detection.
## Set up
```
$ tinygo build -o spring4shell.wasm -scheduler=none -target=wasi --no-debug spring4shell.go
$ mkdir -p ~/.trivy/modules
$ cp spring4shell.wasm ~/.trivy/modules
```
It is also available in [GHCR][trivy-module-spring4shell].
You can install it via `trivy module install`.
```bash
$ trivy module install ghcr.io/aquasecurity/trivy-module-spring4shell
2022-06-13T15:32:21.972+0300 INFO Installing the module from ghcr.io/aquasecurity/trivy-module-spring4shell...
```
## Run Trivy
```
$ trivy image spring-core-rce-jdk8:latest
2022-05-29T22:35:04.873+0300 INFO Loading spring4shell.wasm...
2022-05-29T22:35:05.348+0300 INFO Registering WASM module: spring4shell@v1
2022-05-29T22:35:07.124+0300 INFO Module spring4shell: analyzing /app/tomcat/RELEASE-NOTES...
2022-05-29T22:35:07.139+0300 INFO Module spring4shell: analyzing /app/jdk9/release...
2022-05-29T22:37:04.636+0300 INFO Module spring4shell: analyzing /app/jdk9/release...
...
2022-05-29T22:37:08.917+0300 INFO Module spring4shell: Java Version: 8, Tomcat Version: 8.5.77
2022-05-29T22:37:08.917+0300 INFO Module spring4shell: change CVE-2022-22965 severity from CRITICAL to LOW
```
In the above example, the Java version is 8 which is not affected by CVE-2022-22965, so this module changes the severity from CRITICAL to LOW.
## Note
This module is also used for testing in Trivy.
[trivy-module-spring4shell]: https://github.com/orgs/aquasecurity/packages/container/package/trivy-module-spring4shell

View File

@@ -0,0 +1,18 @@
module github.com/aquasecurity/trivy-module-spring4shell
go 1.18
// It points to local Trivy for testing. Normal WASM modules don't need the replace directive.
replace github.com/aquasecurity/trivy => ../../../
require github.com/aquasecurity/trivy v0.0.0-00010101000000-000000000000
require (
github.com/aquasecurity/fanal v0.0.0-20220614123434-09d6aced4205 // indirect
github.com/aquasecurity/trivy-db v0.0.0-20220602091213-39d8a6798e07 // indirect
github.com/caarlos0/env/v6 v6.9.3 // indirect
github.com/google/go-containerregistry v0.7.1-0.20211214010025-a65b7844a475 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,283 @@
//go:generate tinygo build -o spring4shell.wasm -scheduler=none -target=wasi --no-debug spring4shell.go
//go:build tinygo.wasm
package main
import (
"bufio"
"fmt"
"io"
"os"
"regexp"
"strconv"
"strings"
"github.com/aquasecurity/trivy/pkg/module/api"
"github.com/aquasecurity/trivy/pkg/module/serialize"
"github.com/aquasecurity/trivy/pkg/module/wasm"
"github.com/aquasecurity/trivy/pkg/types"
)
const (
ModuleVersion = 1
ModuleName = "spring4shell"
TypeJavaMajor = ModuleName + "/java-major-version"
TypeTomcatVersion = ModuleName + "/tomcat-version"
)
var (
tomcatVersionRegex = regexp.MustCompile(`Apache Tomcat Version ([\d.]+)`)
)
// main is required for TinyGo to compile to Wasm.
func main() {
wasm.RegisterModule(Spring4Shell{})
}
type Spring4Shell struct {
// Cannot define fields as modules can't keep state.
}
func (Spring4Shell) Version() int {
return ModuleVersion
}
func (Spring4Shell) Name() string {
return ModuleName
}
func (Spring4Shell) RequiredFiles() []string {
return []string{
`\/openjdk-\d+\/release`, // For OpenJDK version
`\/jdk\d+\/release`, // For JDK version
`tomcat\/RELEASE-NOTES`, // For Tomcat version
}
}
func (s Spring4Shell) Analyze(filePath string) (*serialize.AnalysisResult, error) {
wasm.Info(fmt.Sprintf("analyzing %s...", filePath))
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer f.Close()
switch {
case strings.HasSuffix(filePath, "/release"):
return s.parseJavaRelease(f, filePath)
case strings.HasSuffix(filePath, "/RELEASE-NOTES"):
return s.parseTomcatReleaseNotes(f, filePath)
}
return nil, nil
}
// Parse a jdk release file like "/usr/local/openjdk-11/release"
func (Spring4Shell) parseJavaRelease(f *os.File, filePath string) (*serialize.AnalysisResult, error) {
var javaVersion string
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "JAVA_VERSION=") {
continue
}
ss := strings.Split(line, "=")
if len(ss) != 2 {
return nil, fmt.Errorf("invalid java version: %s", line)
}
javaVersion = strings.Trim(ss[1], `"`)
}
if err := scanner.Err(); err != nil {
return nil, err
}
return &serialize.AnalysisResult{
CustomResources: []serialize.CustomResource{
{
Type: TypeJavaMajor,
FilePath: filePath,
Data: javaVersion,
},
},
}, nil
}
func (Spring4Shell) parseTomcatReleaseNotes(f *os.File, filePath string) (*serialize.AnalysisResult, error) {
b, err := io.ReadAll(f)
if err != nil {
return nil, err
}
m := tomcatVersionRegex.FindStringSubmatch(string(b))
if len(m) != 2 {
return nil, fmt.Errorf("unknown tomcat release notes format")
}
return &serialize.AnalysisResult{
CustomResources: []serialize.CustomResource{
{
Type: TypeTomcatVersion,
FilePath: filePath,
Data: m[1],
},
},
}, nil
}
func (Spring4Shell) PostScanSpec() serialize.PostScanSpec {
return serialize.PostScanSpec{
Action: api.ActionUpdate, // Update severity
IDs: []string{"CVE-2022-22965"},
}
}
// PostScan takes results including custom resources and detected CVE-2022-22965.
//
// Example input:
// [
// {
// "Target": "",
// "Class": "custom",
// "CustomResources": [
// {
// "Type": "spring4shell/java-major-version",
// "FilePath": "/usr/local/openjdk-8/release",
// "Layer": {
// "Digest": "sha256:d7b564a873af313eb2dbcb1ed0d393c57543e3666bdedcbe5d75841d72b1f791",
// "DiffID": "sha256:ba40706eccba610401e4942e29f50bdf36807f8638942ce20805b359ae3ac1c1"
// },
// "Data": "1.8.0_322"
// },
// {
// "Type": "spring4shell/tomcat-version",
// "FilePath": "/usr/local/tomcat/RELEASE-NOTES",
// "Layer": {
// "Digest": "sha256:59c0978ccb117247fd40d936973c40df89195f60466118c5acc6a55f8ba29f06",
// "DiffID": "sha256:85595543df2b1115a18284a8ef62d0b235c4bc29e3d33b55f89b54ee1eadf4c6"
// },
// "Data": "8.5.77"
// }
// ]
// },
// {
// "Target": "Java",
// "Class": "lang-pkgs",
// "Type": "jar",
// "Vulnerabilities": [
// {
// "VulnerabilityID": "CVE-2022-22965",
// "PkgName": "org.springframework.boot:spring-boot",
// "PkgPath": "usr/local/tomcat/webapps/helloworld.war",
// "InstalledVersion": "2.6.3",
// "FixedVersion": "2.5.12, 2.6.6",
// "Layer": {
// "Digest": "sha256:cc44af318e91e6f9f9bf73793fa4f0639487613f46aa1f819b02b6e8fb5c6c07",
// "DiffID": "sha256:eb769943b91f10a0418f2fc3b4a4fde6c6293be60c37293fcc0fa319edaf27a5"
// },
// "SeveritySource": "nvd",
// "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-22965",
// "DataSource": {
// "ID": "glad",
// "Name": "GitLab Advisory Database Community",
// "URL": "https://gitlab.com/gitlab-org/advisories-community"
// },
// "Title": "spring-framework: RCE via Data Binding on JDK 9+",
// "Description": "A Spring MVC or Spring WebFlux application running on JDK 9+ may be vulnerable to remote code execution (RCE) via data binding. The specific exploit requires the application to run on Tomcat as a WAR deployment. If the application is deployed as a Spring Boot executable jar, i.e. the default, it is not vulnerable to the exploit. However, the nature of the vulnerability is more general, and there may be other ways to exploit it.",
// "Severity": "CRITICAL",
// "CweIDs": [
// "CWE-94"
// ],
// "VendorSeverity": {
// "ghsa": 4,
// "nvd": 4,
// "redhat": 3
// },
// "CVSS": {
// "ghsa": {
// "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
// "V3Score": 9.8
// },
// "nvd": {
// "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P",
// "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
// "V2Score": 7.5,
// "V3Score": 9.8
// },
// "redhat": {
// "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
// "V3Score": 8.1
// }
// },
// "References": [
// "https://github.com/advisories/GHSA-36p3-wjmg-h94x"
// ],
// "PublishedDate": "2022-04-01T23:15:00Z",
// "LastModifiedDate": "2022-05-19T14:21:00Z"
// }
// ]
// }
//]
func (Spring4Shell) PostScan(results serialize.Results) (serialize.Results, error) {
var javaMajorVersion int
var tomcatVersion string
for _, result := range results {
if result.Class != types.ClassCustom {
continue
}
for _, c := range result.CustomResources {
if c.Type == TypeJavaMajor {
v := c.Data.(string)
ss := strings.Split(v, ".")
if len(ss) == 0 || len(ss) < 2 {
wasm.Warn("Invalid Java version: " + v)
continue
}
ver := ss[0]
if ver == "1" {
ver = ss[1]
}
var err error
javaMajorVersion, err = strconv.Atoi(ver)
if err != nil {
wasm.Warn("Invalid Java version: " + v)
continue
}
} else if c.Type == TypeTomcatVersion {
tomcatVersion = c.Data.(string)
}
}
}
wasm.Info(fmt.Sprintf("Java Version: %d, Tomcat Version: %s", javaMajorVersion, tomcatVersion))
vulnerable := true
// TODO: version comparison
if tomcatVersion == "10.0.20" || tomcatVersion == "9.0.62" || tomcatVersion == "8.5.78" {
vulnerable = false
} else if javaMajorVersion <= 8 {
vulnerable = false
}
for i, result := range results {
for j, vuln := range result.Vulnerabilities {
// Look up Spring4Shell
if vuln.VulnerabilityID != "CVE-2022-22965" {
continue
}
// If it doesn't satisfy any of requirements, the severity should be changed to LOW.
if !strings.Contains(vuln.PkgPath, ".war") || !vulnerable {
wasm.Info(fmt.Sprintf("change %s CVE-2022-22965 severity from CRITICAL to LOW", vuln.PkgName))
results[i].Vulnerabilities[j].Severity = "LOW"
}
}
}
return results, nil
}

View File

@@ -0,0 +1,31 @@
# WoredPress module
This module provides a more in-depth investigation of Wordpress detection.
## Set up
```
$ tinygo build -o wordpress.wasm -scheduler=none -target=wasi --no-debug wordpress.go
$ mkdir -p ~/.trivy/modules
$ cp wordpress.wasm ~/.trivy/modules
```
It is also available in [GHCR][trivy-module-wordpress].
You can install it via `trivy module install`.
```bash
$ trivy module install ghcr.io/aquasecurity/trivy-module-wordpress
2022-06-13T15:32:21.972+0300 INFO Installing the module from ghcr.io/aquasecurity/trivy-module-wordpress...
```
## Run Trivy
```
$ trivy image wordpress:5.7.1
2022-05-29T22:35:04.873+0300 INFO Loading wordpress.wasm...
2022-05-29T22:35:05.348+0300 INFO Registering WASM module: wordpress@v1
```
In the above example, CVE-2020-36326 and CVE-2018-19296 will be detected if the WordPress version is vulnerable.
[trivy-module-wordpress]: https://github.com/orgs/aquasecurity/packages/container/package/trivy-module-wordpress

View File

@@ -0,0 +1,21 @@
module github.com/aquasecurity/trivy-module-spring4shell
go 1.18
// It points to local Trivy for testing. Normal WASM modules don't need the replace directive.
replace github.com/aquasecurity/trivy => ../../../
require (
github.com/aquasecurity/trivy v0.0.0-00010101000000-000000000000
github.com/aquasecurity/trivy-db v0.0.0-20220602091213-39d8a6798e07
github.com/hashicorp/go-version v1.4.0
)
require (
github.com/aquasecurity/fanal v0.0.0-20220614123434-09d6aced4205 // indirect
github.com/caarlos0/env/v6 v6.9.3 // indirect
github.com/google/go-containerregistry v0.7.1-0.20211214010025-a65b7844a475 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
//go:build tinygo.wasm
package main
import (
"bufio"
"fmt"
"os"
"strings"
"github.com/hashicorp/go-version"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy/pkg/module/api"
"github.com/aquasecurity/trivy/pkg/module/serialize"
"github.com/aquasecurity/trivy/pkg/module/wasm"
"github.com/aquasecurity/trivy/pkg/types"
)
const (
moduleVersion = 1
moduleName = "wordpress"
typeWPVersion = "wordpress-version"
)
// main is required for TinyGo to compile to Wasm.
func main() {
wasm.RegisterModule(WordpressModule{})
}
type WordpressModule struct {
// Cannot define fields as modules can't keep state.
}
func (WordpressModule) Version() int {
return moduleVersion
}
func (WordpressModule) Name() string {
return moduleName
}
func (WordpressModule) RequiredFiles() []string {
return []string{
`wp-includes\/version.php`,
}
}
func (s 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], `''";`)
wasm.Info(fmt.Sprintf("WordPress Version: %s", wpVersion))
}
if err = scanner.Err(); err != nil {
return nil, err
}
return &serialize.AnalysisResult{
CustomResources: []serialize.CustomResource{
{
Type: typeWPVersion,
FilePath: filePath,
Data: wpVersion,
},
},
}, nil
}
func (WordpressModule) PostScanSpec() serialize.PostScanSpec {
return serialize.PostScanSpec{
Action: api.ActionInsert, // Add new vulnerabilities
}
}
func (WordpressModule) PostScan(results serialize.Results) (serialize.Results, error) {
wasm.Info("post scan")
// https://wpscan.com/vulnerability/4cd46653-4470-40ff-8aac-318bee2f998d
affectedVersion, err := version.NewConstraint(">=5.7, <5.7.2")
if err != nil {
return nil, err
}
var (
vulnerable bool
wpPath, wpVersion string
)
for _, result := range results {
if result.Class != types.ClassCustom {
continue
}
for _, c := range result.CustomResources {
if c.Type != typeWPVersion {
continue
}
wpPath = c.FilePath
wpVersion = c.Data.(string)
wasm.Info(fmt.Sprintf("WordPress Version: %s", wpVersion))
ver, err := version.NewVersion(wpVersion)
if err != nil {
return nil, err
}
if affectedVersion.Check(ver) {
vulnerable = true
}
break
}
}
if vulnerable {
// Add CVE-2020-36326 and CVE-2018-19296
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",
},
},
{
VulnerabilityID: "CVE-2018-19296",
PkgName: "wordpress",
InstalledVersion: wpVersion,
FixedVersion: "5.7.2",
Vulnerability: dbTypes.Vulnerability{
Title: "PHPMailer before 5.2.27 and 6.x before 6.0.6 is vulnerable to an object injection attack.",
Severity: "HIGH",
},
},
},
})
}
return results, nil
}

257
go.mod
View File

@@ -13,7 +13,9 @@ require (
github.com/aquasecurity/go-npm-version v0.0.0-20201110091526-0b796d180798
github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46
github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492
github.com/aquasecurity/table v1.5.1
github.com/aquasecurity/trivy-db v0.0.0-20220602091213-39d8a6798e07
github.com/aquasecurity/trivy-kubernetes v0.3.1-0.20220613131930-79b2cb425b18
github.com/caarlos0/env/v6 v6.9.3
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/cheggaaa/pb/v3 v3.0.8
@@ -29,23 +31,22 @@ require (
github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f
github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d
github.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936
github.com/mailru/easyjson v0.7.6
github.com/masahiro331/go-mvn-version v0.0.0-20210429150710-d3157d602a08
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/open-policy-agent/opa v0.41.0
github.com/owenrumney/go-sarif/v2 v2.1.1
github.com/package-url/packageurl-go v0.1.1-0.20220203205134-d70459300c8a
github.com/samber/lo v1.21.0
github.com/stretchr/testify v1.7.2
github.com/testcontainers/testcontainers-go v0.13.0
github.com/tetratelabs/wazero v0.0.0-20220606011721-119b069ba23e
github.com/twitchtv/twirp v8.1.2+incompatible
github.com/urfave/cli/v2 v2.8.1
go.uber.org/zap v1.21.0
golang.org/x/exp v0.0.0-20220407100705-7b9b53b0aca4
golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
google.golang.org/protobuf v1.28.0
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
)
@@ -65,45 +66,90 @@ require (
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/GoogleCloudPlatform/docker-credential-gcr v2.0.5+incompatible // indirect
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Masterminds/squirrel v1.5.2 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/Microsoft/hcsshim v0.9.2 // indirect
github.com/OneOfOne/xxhash v1.2.8 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/VividCortex/ewma v1.1.1 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/agnivade/levenshtein v1.0.1 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/apparentlymart/go-cidr v1.1.0 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/aquasecurity/defsec v0.63.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
github.com/aws/aws-sdk-go v1.44.25 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/bmatcuk/doublestar v1.3.4 // indirect
github.com/briandowns/spinner v1.12.0 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
github.com/container-orchestrated-devices/container-device-interface v0.3.1 // indirect
github.com/containerd/cgroups v1.0.3 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/containerd/containerd v1.6.4 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/containerd/fifo v1.0.0 // indirect
github.com/containerd/go-cni v1.1.6 // indirect
github.com/containerd/imgcrypt v1.1.5-0.20220421044638-8ba028dca028 // indirect
github.com/containerd/nerdctl v0.20.0 // indirect
github.com/containerd/stargz-snapshotter v0.11.4 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.11.4 // indirect
github.com/containerd/ttrpc v1.1.1-0.20220420014843-944ef4a40df3 // indirect
github.com/containerd/typeurl v1.0.3-0.20220422153119-7f6e6d160d67 // indirect
github.com/containernetworking/cni v1.1.1 // indirect
github.com/containers/ocicrypt v1.1.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/docker/cli v20.10.16+incompatible // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/emicklei/go-restful v2.9.5+incompatible // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
github.com/go-git/go-git/v5 v5.4.2 // indirect
github.com/go-gorp/gorp/v3 v3.0.2 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-yaml v1.8.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gosuri/uitable v0.0.4 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1
@@ -114,160 +160,51 @@ require (
github.com/hashicorp/hcl/v2 v2.12.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/ipfs/go-cid v0.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jmoiron/sqlx v1.3.4 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/klauspost/compress v1.15.1 // indirect
github.com/klauspost/cpuid/v2 v2.0.6 // indirect
github.com/knqyf263/go-rpmdb v0.0.0-20220607073645-842f01763e21 // indirect
github.com/knqyf263/nested v0.0.1 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/liamg/iamgo v0.0.6 // indirect
github.com/liamg/jfather v0.0.7 // indirect
github.com/liamg/memoryfs v1.4.2 // indirect
github.com/liamg/memoryfs v1.4.2
github.com/liamg/tml v0.6.0
github.com/lib/pq v1.10.4 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/buildkit v0.10.3 // indirect
github.com/moby/sys/mount v0.3.2 // indirect
github.com/moby/sys/mountinfo v0.6.1 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20220303224323-02efb9a75ee1 // indirect
github.com/opencontainers/runc v1.1.2 // indirect
github.com/owenrumney/squealer v1.0.1-0.20220510063705-c0be93f0edea // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/saracen/walker v0.0.0-20191201085201-324a081bae7e // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spdx/tools-golang v0.3.0
github.com/spf13/cast v1.4.1 // indirect
github.com/stretchr/objx v0.3.0 // indirect
github.com/ulikunitz/xz v0.5.8 // indirect
github.com/vbatts/tar-split v0.11.2 // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/yashtewari/glob-intersection v0.1.0 // indirect
github.com/zclconf/go-cty v1.10.0 // indirect
github.com/zclconf/go-cty-yaml v1.0.2 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.opencensus.io v0.23.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.7.0 // indirect
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/net v0.0.0-20220516133312-45b265872317 // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717 // indirect
google.golang.org/api v0.62.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220426171045-31bebdecfb46 // indirect
google.golang.org/grpc v1.47.0 // indirect
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
gopkg.in/go-playground/validator.v9 v9.31.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
lukechampine.com/uint128 v1.1.1 // indirect
modernc.org/cc/v3 v3.36.0 // indirect
modernc.org/ccgo/v3 v3.16.6 // indirect
modernc.org/libc v1.16.7 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.1.1 // indirect
modernc.org/opt v0.1.1 // indirect
modernc.org/sqlite v1.17.3 // indirect
modernc.org/strutil v1.1.1 // indirect
modernc.org/token v1.0.0 // indirect
)
require (
github.com/aquasecurity/table v1.5.1
github.com/aquasecurity/trivy-kubernetes v0.3.1-0.20220613131930-79b2cb425b18
gotest.tools v2.2.0+incompatible
)
require (
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect
github.com/Masterminds/squirrel v1.5.2 // indirect
github.com/Microsoft/hcsshim v0.9.2 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/agnivade/levenshtein v1.0.1 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
github.com/container-orchestrated-devices/container-device-interface v0.3.1 // indirect
github.com/containerd/cgroups v1.0.3 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/containerd/fifo v1.0.0 // indirect
github.com/containerd/go-cni v1.1.6 // indirect
github.com/containerd/imgcrypt v1.1.5-0.20220421044638-8ba028dca028 // indirect
github.com/containerd/nerdctl v0.20.0 // indirect
github.com/containerd/stargz-snapshotter v0.11.4 // indirect
github.com/containerd/ttrpc v1.1.1-0.20220420014843-944ef4a40df3 // indirect
github.com/containernetworking/cni v1.1.1 // indirect
github.com/containers/ocicrypt v1.1.3 // indirect
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/emicklei/go-restful v2.9.5+incompatible // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-gorp/gorp/v3 v3.0.2 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gosuri/uitable v0.0.4 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/ipfs/go-cid v0.1.0 // indirect
github.com/jmoiron/sqlx v1.3.4 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.0.6 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/lib/pq v1.10.4 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/sys/mount v0.3.2 // indirect
github.com/moby/sys/mountinfo v0.6.1 // indirect
github.com/moby/sys/signal v0.7.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.0.3 // indirect
github.com/multiformats/go-base36 v0.1.0 // indirect
@@ -275,34 +212,82 @@ require (
github.com/multiformats/go-multihash v0.0.15 // indirect
github.com/multiformats/go-varint v0.0.6 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20220303224323-02efb9a75ee1 // indirect
github.com/opencontainers/runc v1.1.2 // indirect
github.com/opencontainers/runtime-spec v1.0.3-0.20220311020903-6969a0a09ab1 // indirect
github.com/opencontainers/runtime-tools v0.0.0-20190417131837-cd1349b7c47e // indirect
github.com/opencontainers/selinux v1.10.1 // indirect
github.com/owenrumney/squealer v1.0.1-0.20220510063705-c0be93f0edea // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rubenv/sql-migrate v1.1.1 // indirect
github.com/russross/blackfriday v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/saracen/walker v0.0.0-20191201085201-324a081bae7e // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spdx/tools-golang v0.3.0
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/cobra v1.4.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 // indirect
github.com/stretchr/objx v0.3.0 // indirect
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
github.com/tidwall/gjson v1.14.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/ulikunitz/xz v0.5.8 // indirect
github.com/urfave/cli v1.22.9 // indirect
github.com/vbatts/tar-split v0.11.2 // indirect
github.com/vektah/gqlparser/v2 v2.4.4 // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/yashtewari/glob-intersection v0.1.0 // indirect
github.com/zclconf/go-cty v1.10.0 // indirect
github.com/zclconf/go-cty-yaml v1.0.2 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 // indirect
go.opencensus.io v0.23.0 // indirect
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.7.0 // indirect
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/net v0.0.0-20220516133312-45b265872317 // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect
golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e // indirect
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717 // indirect
google.golang.org/api v0.62.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220426171045-31bebdecfb46 // indirect
google.golang.org/grpc v1.47.0 // indirect
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
gopkg.in/go-playground/validator.v9 v9.31.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gotest.tools v2.2.0+incompatible
helm.sh/helm/v3 v3.9.0 // indirect
k8s.io/api v0.24.1 // indirect
k8s.io/apiextensions-apiserver v0.24.0 // indirect
@@ -314,6 +299,16 @@ require (
k8s.io/klog/v2 v2.60.1 // indirect
k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect
k8s.io/kubectl v0.24.1 // indirect
lukechampine.com/uint128 v1.1.1 // indirect
modernc.org/cc/v3 v3.36.0 // indirect
modernc.org/ccgo/v3 v3.16.6 // indirect
modernc.org/libc v1.16.7 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.1.1 // indirect
modernc.org/opt v0.1.1 // indirect
modernc.org/sqlite v1.17.3 // indirect
modernc.org/strutil v1.1.1 // indirect
modernc.org/token v1.0.0 // indirect
oras.land/oras-go v1.1.1 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/kustomize/api v0.11.4 // indirect

2
go.sum
View File

@@ -1203,6 +1203,8 @@ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG
github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/testcontainers/testcontainers-go v0.13.0 h1:OUujSlEGsXVo/ykPVZk3KanBNGN0TYb/7oKIPVn15JA=
github.com/testcontainers/testcontainers-go v0.13.0/go.mod h1:z1abufU633Eb/FmSBTzV6ntZAC1eZBYPtaFsn4nPuDk=
github.com/tetratelabs/wazero v0.0.0-20220606011721-119b069ba23e h1:ZamZ43RLjKymhv0ny0uXU1cUL8T/UEXmTQRJLAvJcF4=
github.com/tetratelabs/wazero v0.0.0-20220606011721-119b069ba23e/go.mod h1:Y4X/zO4sC2dJjZG9GDYNRbJGogfqFYJY/BbyKlOxXGI=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=

View File

@@ -526,6 +526,9 @@ func setup(t *testing.T, options setupOptions) (*cli.App, string, string) {
// Set up testing DB
cacheDir := initDB(t)
// Set a temp dir so that modules will not be loaded
t.Setenv("XDG_DATA_HOME", cacheDir)
port, err := getFreePort()
assert.NoError(t, err)
addr := fmt.Sprintf("localhost:%d", port)

View File

@@ -199,6 +199,9 @@ func TestDockerEngine(t *testing.T) {
// Set up testing DB
cacheDir := initDB(t)
// Set a temp dir so that modules will not be loaded
t.Setenv("XDG_DATA_HOME", cacheDir)
ctx := context.Background()
defer ctx.Done()

View File

@@ -131,6 +131,9 @@ func TestFilesystem(t *testing.T) {
// Set up testing DB
cacheDir := initDB(t)
// Set a temp dir so that modules will not be loaded
t.Setenv("XDG_DATA_HOME", cacheDir)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
osArgs := []string{

View File

@@ -1,5 +1,4 @@
//go:build integration
// +build integration
//go:build integration || module_integration
package integration
@@ -10,6 +9,7 @@ import (
"net"
"os"
"path/filepath"
"sort"
"testing"
"time"
@@ -108,6 +108,13 @@ func readReport(t *testing.T, filePath string) types.Report {
for j := range result.Vulnerabilities {
report.Results[i].Vulnerabilities[j].Layer.Digest = ""
}
sort.Slice(result.CustomResources, func(i, j int) bool {
if result.CustomResources[i].Type != result.CustomResources[j].Type {
return result.CustomResources[i].Type < result.CustomResources[j].Type
}
return result.CustomResources[i].FilePath < result.CustomResources[j].FilePath
})
}
return report

View File

@@ -0,0 +1,75 @@
//go:build module_integration
package integration
import (
"io"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/aquasecurity/trivy/pkg/commands"
"github.com/aquasecurity/trivy/pkg/module"
"github.com/aquasecurity/trivy/pkg/utils"
)
func TestModule(t *testing.T) {
tests := []struct {
name string
input string
golden string
}{
{
name: "spring4shell jre 8, severity update",
input: "testdata/fixtures/images/spring4shell-jre8.tar.gz",
golden: "testdata/spring4shell-jre8.json.golden",
},
{
name: "spring4shell jre 11, no severity update",
input: "testdata/fixtures/images/spring4shell-jre11.tar.gz",
golden: "testdata/spring4shell-jre11.json.golden",
},
}
// Set up testing DB
cacheDir := initDB(t)
// Set up module dir
moduleDir := filepath.Join(cacheDir, module.RelativeDir)
err := os.MkdirAll(moduleDir, 0700)
require.NoError(t, err)
// Set up Spring4Shell module
t.Setenv("XDG_DATA_HOME", cacheDir)
_, err = utils.CopyFile(filepath.Join("../", "examples", "module", "spring4shell", "spring4shell.wasm"),
filepath.Join(moduleDir, "spring4shell.wasm"))
require.NoError(t, err)
// Setup CLI App
app := commands.NewApp("dev")
app.Writer = io.Discard
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
osArgs := []string{"trivy", "--cache-dir", cacheDir, "image", "--ignore-unfixed", "--format", "json",
"--skip-update", "--offline-scan", "--input", tt.input}
// Set up the output file
outputFile := filepath.Join(t.TempDir(), "output.json")
if *update {
outputFile = tt.golden
}
osArgs = append(osArgs, []string{"--output", outputFile}...)
// Run Trivy
assert.Nil(t, app.Run(osArgs))
// Compare want and got
compareReports(t, tt.golden, outputFile)
})
}
}

View File

@@ -221,6 +221,9 @@ func scan(t *testing.T, imageRef name.Reference, baseDir, goldenFile string, opt
// Set up testing DB
cacheDir := initDB(t)
// Set a temp dir so that modules will not be loaded
t.Setenv("XDG_DATA_HOME", cacheDir)
// Setup the output file
outputFile := filepath.Join(t.TempDir(), "output.json")
if *update {

View File

@@ -4,6 +4,8 @@ TEST_IMAGE=ghcr.io/aquasecurity/trivy-test-images
CURRENT=$(cd $(dirname $0);pwd)
mkdir -p ${CURRENT}/../testdata/fixtures/images/
# List the tags
TAGS=$(crane ls ${TEST_IMAGE})

View File

@@ -261,6 +261,9 @@ func TestTar(t *testing.T) {
// Set up testing DB
cacheDir := initDB(t)
// Set a temp dir so that modules will not be loaded
t.Setenv("XDG_DATA_HOME", cacheDir)
// Setup CLI App
app := commands.NewApp("dev")
app.Writer = io.Discard

View File

@@ -8,6 +8,14 @@
- 2.9.10.4
VulnerableVersions:
- ">= 2.0.0, <= 2.9.10.3"
- bucket: org.springframework:spring-beans
pairs:
- key: CVE-2022-22965
value:
PatchedVersions:
- 5.3.18
VulnerableVersions:
- ">= 5.3.0, < 5.3.18"
- bucket: maven::GitLab Advisory Database Community
pairs:
- bucket: com.fasterxml.jackson.core:jackson-databind

View File

@@ -1179,3 +1179,31 @@
- https://go-review.googlesource.com/c/text/+/340830
- https://go.googlesource.com/text/+/383b2e75a7a4198c42f8f87833eefb772868a56f
- https://pkg.go.dev/vuln/GO-2021-0113
- key: CVE-2022-22965
value:
Title: "spring-framework: RCE via Data Binding on JDK 9+"
Description: "A Spring MVC or Spring WebFlux application running on JDK 9+ may be vulnerable to remote code execution (RCE) via data binding. The specific exploit requires the application to run on Tomcat as a WAR deployment. If the application is deployed as a Spring Boot executable jar, i.e. the default, it is not vulnerable to the exploit. However, the nature of the vulnerability is more general, and there may be other ways to exploit it."
Severity: CRITICAL
CweIDs:
- CWE-94
VendorSeverity:
nvd: 4
ghsa: 4
redhat: 3
CVSS:
ghsa:
V3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
V3Score: 9.8
nvd:
V2Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P"
V3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
V2Score: 7.5
V3Score: 9.8
redhat:
V3Vector: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H"
V3Score: 8.1
References:
- "https://github.com/advisories/GHSA-36p3-wjmg-h94x",
PublishedDate: "2022-04-01T23:15:00Z"
LastModifiedDate: "2022-05-19T14:21:00Z"

View File

@@ -0,0 +1,266 @@
{
"SchemaVersion": 2,
"ArtifactName": "testdata/fixtures/images/spring4shell-jre11.tar.gz",
"ArtifactType": "container_image",
"Metadata": {
"OS": {
"Family": "debian",
"Name": "11.3"
},
"ImageID": "sha256:ed8f0747d483b60657982f0ef1ba74482aed08795cf0eb774b00bc53022a8351",
"DiffIDs": [
"sha256:608f3a074261105f129d707e4d9ad3d41b5baa94887f092b7c2857f7274a2fce",
"sha256:1f6e409d1c59c8e06608a024b82d50490313abc3b2ff93730e43135d5be0cd72",
"sha256:1f0e278ace87a84577de56c99e5c05c6af6f8b582d1eb8dfd7de7be4cf215775",
"sha256:64272e9218cd019d57b84ac283aa35036cbd8c1dcface8c69f756088a0a13c45",
"sha256:8e6776c643c1db15d540016171fe04137ee2a26c7d0b18bfebdcbd31c6b0d8b3",
"sha256:0b201a611e5455d637c719d70eb5dd76fd4154bc4a5cf597d67ed2fb6647cc42",
"sha256:19da2426772aaa344a242e474fd7906d272fc8ded6eef5b4e461a4aa0725d7e5",
"sha256:1fdc094b0e85888d2204310083e3c09fff6a4daeecf22692aa6be5e8b4001f94",
"sha256:192960b65b1579403b36581de471fd2bd75a043b4743552f27ba16623f02c68f"
],
"ImageConfig": {
"architecture": "amd64",
"created": "2022-06-07T03:41:13.228952Z",
"docker_version": "20.10.14",
"history": [
{
"created": "2022-03-29T00:22:18.812238611Z",
"created_by": "/bin/sh -c #(nop) ADD file:966d3669b40f5fbaecee1ecbeb58debe19001076da5d94717080d55efbc25971 in / "
},
{
"created": "2022-03-29T00:22:19.186561403Z",
"created_by": "/bin/sh -c #(nop) CMD [\"bash\"]",
"empty_layer": true
},
{
"created": "2022-03-29T00:52:15.681202963Z",
"created_by": "/bin/sh -c set -eux; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tca-certificates p11-kit \t; \trm -rf /var/lib/apt/lists/*"
},
{
"created": "2022-03-29T00:55:28.571451389Z",
"created_by": "/bin/sh -c #(nop) ENV JAVA_HOME=/usr/local/openjdk-11",
"empty_layer": true
},
{
"created": "2022-03-29T00:55:29.092016566Z",
"created_by": "/bin/sh -c { echo '#/bin/sh'; echo 'echo \"$JAVA_HOME\"'; } \u003e /usr/local/bin/docker-java-home \u0026\u0026 chmod +x /usr/local/bin/docker-java-home \u0026\u0026 [ \"$JAVA_HOME\" = \"$(docker-java-home)\" ] # backwards compatibility"
},
{
"created": "2022-03-29T00:55:29.206969756Z",
"created_by": "/bin/sh -c #(nop) ENV PATH=/usr/local/openjdk-11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"empty_layer": true
},
{
"created": "2022-03-29T00:55:29.302995298Z",
"created_by": "/bin/sh -c #(nop) ENV LANG=C.UTF-8",
"empty_layer": true
},
{
"created": "2022-03-29T00:55:29.392969112Z",
"created_by": "/bin/sh -c #(nop) ENV JAVA_VERSION=11.0.14.1",
"empty_layer": true
},
{
"created": "2022-03-29T00:56:45.085797918Z",
"created_by": "/bin/sh -c set -eux; \t\tarch=\"$(dpkg --print-architecture)\"; \tcase \"$arch\" in \t\t'amd64') \t\t\tdownloadUrl='https://github.com/AdoptOpenJDK/openjdk11-upstream-binaries/releases/download/jdk-11.0.14.1%2B1/OpenJDK11U-jre_x64_linux_11.0.14.1_1.tar.gz'; \t\t\t;; \t\t'arm64') \t\t\tdownloadUrl='https://github.com/AdoptOpenJDK/openjdk11-upstream-binaries/releases/download/jdk-11.0.14.1%2B1/OpenJDK11U-jre_aarch64_linux_11.0.14.1_1.tar.gz'; \t\t\t;; \t\t*) echo \u003e\u00262 \"error: unsupported architecture: '$arch'\"; exit 1 ;; \tesac; \t\tsavedAptMark=\"$(apt-mark showmanual)\"; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tdirmngr \t\tgnupg \t\twget \t; \trm -rf /var/lib/apt/lists/*; \t\twget --progress=dot:giga -O openjdk.tgz \"$downloadUrl\"; \twget --progress=dot:giga -O openjdk.tgz.asc \"$downloadUrl.sign\"; \t\texport GNUPGHOME=\"$(mktemp -d)\"; \tgpg --batch --keyserver keyserver.ubuntu.com --recv-keys EAC843EBD3EFDB98CC772FADA5CD6035332FA671; \tgpg --batch --keyserver keyserver.ubuntu.com --keyserver-options no-self-sigs-only --recv-keys CA5F11C6CE22644D42C6AC4492EF8D39DC13168F; \tgpg --batch --list-sigs --keyid-format 0xLONG CA5F11C6CE22644D42C6AC4492EF8D39DC13168F \t\t| tee /dev/stderr \t\t| grep '0xA5CD6035332FA671' \t\t| grep 'Andrew Haley'; \tgpg --batch --verify openjdk.tgz.asc openjdk.tgz; \tgpgconf --kill all; \trm -rf \"$GNUPGHOME\"; \t\tmkdir -p \"$JAVA_HOME\"; \ttar --extract \t\t--file openjdk.tgz \t\t--directory \"$JAVA_HOME\" \t\t--strip-components 1 \t\t--no-same-owner \t; \trm openjdk.tgz*; \t\tapt-mark auto '.*' \u003e /dev/null; \t[ -z \"$savedAptMark\" ] || apt-mark manual $savedAptMark \u003e /dev/null; \tapt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \t\t{ \t\techo '#!/usr/bin/env bash'; \t\techo 'set -Eeuo pipefail'; \t\techo 'trust extract --overwrite --format=java-cacerts --filter=ca-anchors --purpose=server-auth \"$JAVA_HOME/lib/security/cacerts\"'; \t} \u003e /etc/ca-certificates/update.d/docker-openjdk; \tchmod +x /etc/ca-certificates/update.d/docker-openjdk; \t/etc/ca-certificates/update.d/docker-openjdk; \t\tfind \"$JAVA_HOME/lib\" -name '*.so' -exec dirname '{}' ';' | sort -u \u003e /etc/ld.so.conf.d/docker-openjdk.conf; \tldconfig; \t\tjava -Xshare:dump; \t\tjava --version"
},
{
"created": "2022-03-30T05:16:56.493239413Z",
"created_by": "/bin/sh -c #(nop) ENV CATALINA_HOME=/usr/local/tomcat",
"empty_layer": true
},
{
"created": "2022-03-30T05:16:56.592339446Z",
"created_by": "/bin/sh -c #(nop) ENV PATH=/usr/local/tomcat/bin:/usr/local/openjdk-11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"empty_layer": true
},
{
"created": "2022-03-30T05:16:57.135799132Z",
"created_by": "/bin/sh -c mkdir -p \"$CATALINA_HOME\""
},
{
"created": "2022-03-30T05:16:57.234962251Z",
"created_by": "/bin/sh -c #(nop) WORKDIR /usr/local/tomcat",
"empty_layer": true
},
{
"created": "2022-03-30T05:16:57.332478398Z",
"created_by": "/bin/sh -c #(nop) ENV TOMCAT_NATIVE_LIBDIR=/usr/local/tomcat/native-jni-lib",
"empty_layer": true
},
{
"created": "2022-03-30T05:16:57.423152329Z",
"created_by": "/bin/sh -c #(nop) ENV LD_LIBRARY_PATH=/usr/local/tomcat/native-jni-lib",
"empty_layer": true
},
{
"created": "2022-03-30T05:38:59.455604207Z",
"created_by": "/bin/sh -c #(nop) ENV GPG_KEYS=05AB33110949707C93A279E3D3EFE6B686867BA6 07E48665A34DCAFAE522E5E6266191C37C037D42 47309207D818FFD8DCD3F83F1931D684307A10A5 541FBE7D8F78B25E055DDEE13C370389288584E7 5C3C5F3E314C866292F359A8F3AD5C94A67F707E 765908099ACF92702C7D949BFA0C35EA8AA299F1 79F7026C690BAA50B92CD8B66A3AD3F4F22C4FED 9BA44C2621385CB966EBA586F72C284D731FABEE A27677289986DB50844682F8ACB77FC2E86E29AC A9C5DF4D22E99998D9875A5110C01C5A2F6059E7 DCFD35E0BF8CA7344752DE8B6FB21E8933C60243 F3A04C595DB5B6A5F1ECA43E3B7BBB100D811BBE F7DA48BB64BCB84ECBA7EE6935CD23C10D498E23",
"empty_layer": true
},
{
"created": "2022-03-30T05:38:59.550766811Z",
"created_by": "/bin/sh -c #(nop) ENV TOMCAT_MAJOR=8",
"empty_layer": true
},
{
"created": "2022-03-30T05:38:59.643674076Z",
"created_by": "/bin/sh -c #(nop) ENV TOMCAT_VERSION=8.5.77",
"empty_layer": true
},
{
"created": "2022-03-30T05:38:59.744285526Z",
"created_by": "/bin/sh -c #(nop) ENV TOMCAT_SHA512=50f96584cbbbeeda92a3b573e7fe7e2c49e57ed4bc5246257dc1409abac0710b49fa7049a0dd9a3b8467bca2aa078ef608f49b676c1abf12529528ff71bb0260",
"empty_layer": true
},
{
"created": "2022-03-30T05:39:00.204794279Z",
"created_by": "/bin/sh -c #(nop) COPY dir:92f3a0f303b55a048a73bf243c664f89aa86500eab95c7d20c2da44ed3fb434b in /usr/local/tomcat "
},
{
"created": "2022-03-30T05:39:03.786979035Z",
"created_by": "/bin/sh -c set -eux; \tapt-get update; \txargs -rt apt-get install -y --no-install-recommends \u003c \"$TOMCAT_NATIVE_LIBDIR/.dependencies.txt\"; \trm -rf /var/lib/apt/lists/*"
},
{
"created": "2022-03-30T05:39:05.151055599Z",
"created_by": "/bin/sh -c set -eux; \tnativeLines=\"$(catalina.sh configtest 2\u003e\u00261)\"; \tnativeLines=\"$(echo \"$nativeLines\" | grep 'Apache Tomcat Native')\"; \tnativeLines=\"$(echo \"$nativeLines\" | sort -u)\"; \tif ! echo \"$nativeLines\" | grep -E 'INFO: Loaded( APR based)? Apache Tomcat Native library' \u003e\u00262; then \t\techo \u003e\u00262 \"$nativeLines\"; \t\texit 1; \tfi"
},
{
"created": "2022-03-30T05:39:05.243348189Z",
"created_by": "/bin/sh -c #(nop) EXPOSE 8080",
"empty_layer": true
},
{
"created": "2022-03-30T05:39:05.342897424Z",
"created_by": "/bin/sh -c #(nop) CMD [\"catalina.sh\" \"run\"]",
"empty_layer": true
},
{
"created": "2022-06-06T13:51:56.544179Z",
"created_by": "/bin/sh -c #(nop) COPY file:4a1136b54136f8775efe918c4cd6af1ad1e507b36a49286d4f2c6bde722d33f4 in /usr/local/tomcat/webapps/ "
}
],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:608f3a074261105f129d707e4d9ad3d41b5baa94887f092b7c2857f7274a2fce",
"sha256:1f6e409d1c59c8e06608a024b82d50490313abc3b2ff93730e43135d5be0cd72",
"sha256:1f0e278ace87a84577de56c99e5c05c6af6f8b582d1eb8dfd7de7be4cf215775",
"sha256:64272e9218cd019d57b84ac283aa35036cbd8c1dcface8c69f756088a0a13c45",
"sha256:8e6776c643c1db15d540016171fe04137ee2a26c7d0b18bfebdcbd31c6b0d8b3",
"sha256:0b201a611e5455d637c719d70eb5dd76fd4154bc4a5cf597d67ed2fb6647cc42",
"sha256:19da2426772aaa344a242e474fd7906d272fc8ded6eef5b4e461a4aa0725d7e5",
"sha256:1fdc094b0e85888d2204310083e3c09fff6a4daeecf22692aa6be5e8b4001f94",
"sha256:192960b65b1579403b36581de471fd2bd75a043b4743552f27ba16623f02c68f"
]
},
"config": {
"Cmd": [
"catalina.sh",
"run"
],
"Env": [
"PATH=/usr/local/tomcat/bin:/usr/local/openjdk-11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"JAVA_HOME=/usr/local/openjdk-11",
"LANG=C.UTF-8",
"JAVA_VERSION=11.0.14.1",
"CATALINA_HOME=/usr/local/tomcat",
"TOMCAT_NATIVE_LIBDIR=/usr/local/tomcat/native-jni-lib",
"LD_LIBRARY_PATH=/usr/local/tomcat/native-jni-lib",
"GPG_KEYS=05AB33110949707C93A279E3D3EFE6B686867BA6 07E48665A34DCAFAE522E5E6266191C37C037D42 47309207D818FFD8DCD3F83F1931D684307A10A5 541FBE7D8F78B25E055DDEE13C370389288584E7 5C3C5F3E314C866292F359A8F3AD5C94A67F707E 765908099ACF92702C7D949BFA0C35EA8AA299F1 79F7026C690BAA50B92CD8B66A3AD3F4F22C4FED 9BA44C2621385CB966EBA586F72C284D731FABEE A27677289986DB50844682F8ACB77FC2E86E29AC A9C5DF4D22E99998D9875A5110C01C5A2F6059E7 DCFD35E0BF8CA7344752DE8B6FB21E8933C60243 F3A04C595DB5B6A5F1ECA43E3B7BBB100D811BBE F7DA48BB64BCB84ECBA7EE6935CD23C10D498E23",
"TOMCAT_MAJOR=8",
"TOMCAT_VERSION=8.5.77",
"TOMCAT_SHA512=50f96584cbbbeeda92a3b573e7fe7e2c49e57ed4bc5246257dc1409abac0710b49fa7049a0dd9a3b8467bca2aa078ef608f49b676c1abf12529528ff71bb0260"
],
"Image": "sha256:8ac2c9cef8f1bb48394c1b2ee81cc1d2096323a7a7cec4781d601eeaf7c32b03",
"WorkingDir": "/usr/local/tomcat",
"ExposedPorts": {
"8080/tcp": {}
}
}
}
},
"Results": [
{
"Target": "testdata/fixtures/images/spring4shell-jre11.tar.gz (debian 11.3)",
"Class": "os-pkgs",
"Type": "debian"
},
{
"Target": "Java",
"Class": "lang-pkgs",
"Type": "jar",
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2022-22965",
"PkgName": "org.springframework:spring-beans",
"PkgPath": "usr/local/tomcat/webapps/helloworld.war",
"InstalledVersion": "5.3.15",
"FixedVersion": "5.3.18",
"Layer": {
"Digest": "sha256:8eeeb69b4f5af871d1bc14ebb077b478a8260542f2c2a3897e8942bd90a8a62a",
"DiffID": "sha256:192960b65b1579403b36581de471fd2bd75a043b4743552f27ba16623f02c68f"
},
"SeveritySource": "ghsa",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-22965",
"DataSource": {
"ID": "ghsa",
"Name": "GitHub Security Advisory Maven",
"URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven"
},
"Title": "spring-framework: RCE via Data Binding on JDK 9+",
"Description": "A Spring MVC or Spring WebFlux application running on JDK 9+ may be vulnerable to remote code execution (RCE) via data binding. The specific exploit requires the application to run on Tomcat as a WAR deployment. If the application is deployed as a Spring Boot executable jar, i.e. the default, it is not vulnerable to the exploit. However, the nature of the vulnerability is more general, and there may be other ways to exploit it.",
"Severity": "CRITICAL",
"CweIDs": [
"CWE-94"
],
"CVSS": {
"ghsa": {
"V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"V3Score": 9.8
},
"nvd": {
"V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P",
"V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"V2Score": 7.5,
"V3Score": 9.8
},
"redhat": {
"V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
"V3Score": 8.1
}
},
"References": [
"https://github.com/advisories/GHSA-36p3-wjmg-h94x"
]
}
]
},
{
"Target": "",
"Class": "custom",
"CustomResources": [
{
"Type": "spring4shell/java-major-version",
"FilePath": "/usr/local/openjdk-11/release",
"Layer": {
"Digest": "sha256:e94fd7d3bf7a9b78b61be8303cd35eb9da3f8d121cf572a3b8878cbf11e84818",
"DiffID": "sha256:64272e9218cd019d57b84ac283aa35036cbd8c1dcface8c69f756088a0a13c45"
},
"Data": "11.0.14.1"
},
{
"Type": "spring4shell/tomcat-version",
"FilePath": "/usr/local/tomcat/RELEASE-NOTES",
"Layer": {
"Digest": "sha256:ac3639dc6fd33e9eeead58a99c277cb06b8f69ba6a30fe7028e9677a67d94bd8",
"DiffID": "sha256:0b201a611e5455d637c719d70eb5dd76fd4154bc4a5cf597d67ed2fb6647cc42"
},
"Data": "8.5.77"
}
]
}
]
}

View File

@@ -0,0 +1,266 @@
{
"SchemaVersion": 2,
"ArtifactName": "testdata/fixtures/images/spring4shell-jre8.tar.gz",
"ArtifactType": "container_image",
"Metadata": {
"OS": {
"Family": "debian",
"Name": "11.3"
},
"ImageID": "sha256:b88bc3d2f0b5aacf1d36efa498f427d923b01c854dac090acf5368c55ac04fda",
"DiffIDs": [
"sha256:608f3a074261105f129d707e4d9ad3d41b5baa94887f092b7c2857f7274a2fce",
"sha256:1f6e409d1c59c8e06608a024b82d50490313abc3b2ff93730e43135d5be0cd72",
"sha256:0e78b1e5673e8cc7c102fdda9e6b830b7dee2b29b178f34d25d9be59387e6950",
"sha256:ba40706eccba610401e4942e29f50bdf36807f8638942ce20805b359ae3ac1c1",
"sha256:053db4876c0df3df3294ee00e32e140b130ba33807d088750cb69b0e6fad158e",
"sha256:85595543df2b1115a18284a8ef62d0b235c4bc29e3d33b55f89b54ee1eadf4c6",
"sha256:868d710aa4dc5fc4793508564fc45c991ed8d5f6ab3e4cf52bb856f29546f3d8",
"sha256:77b2d158369254d5055183f5483f8b6661170857b61768d1d95d18c2ec1714b6",
"sha256:eb769943b91f10a0418f2fc3b4a4fde6c6293be60c37293fcc0fa319edaf27a5"
],
"ImageConfig": {
"architecture": "amd64",
"created": "2022-06-06T13:51:57.120019Z",
"docker_version": "20.10.14",
"history": [
{
"created": "2022-03-29T00:22:18.812238611Z",
"created_by": "/bin/sh -c #(nop) ADD file:966d3669b40f5fbaecee1ecbeb58debe19001076da5d94717080d55efbc25971 in / "
},
{
"created": "2022-03-29T00:22:19.186561403Z",
"created_by": "/bin/sh -c #(nop) CMD [\"bash\"]",
"empty_layer": true
},
{
"created": "2022-03-29T00:52:15.681202963Z",
"created_by": "/bin/sh -c set -eux; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tca-certificates p11-kit \t; \trm -rf /var/lib/apt/lists/*"
},
{
"created": "2022-03-29T00:57:17.289858958Z",
"created_by": "/bin/sh -c #(nop) ENV JAVA_HOME=/usr/local/openjdk-8",
"empty_layer": true
},
{
"created": "2022-03-29T00:57:17.842070347Z",
"created_by": "/bin/sh -c { echo '#/bin/sh'; echo 'echo \"$JAVA_HOME\"'; } \u003e /usr/local/bin/docker-java-home \u0026\u0026 chmod +x /usr/local/bin/docker-java-home \u0026\u0026 [ \"$JAVA_HOME\" = \"$(docker-java-home)\" ] # backwards compatibility"
},
{
"created": "2022-03-29T00:57:17.930626834Z",
"created_by": "/bin/sh -c #(nop) ENV PATH=/usr/local/openjdk-8/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"empty_layer": true
},
{
"created": "2022-03-29T00:57:18.024334574Z",
"created_by": "/bin/sh -c #(nop) ENV LANG=C.UTF-8",
"empty_layer": true
},
{
"created": "2022-03-29T00:57:18.115954625Z",
"created_by": "/bin/sh -c #(nop) ENV JAVA_VERSION=8u322",
"empty_layer": true
},
{
"created": "2022-03-29T00:58:15.033935002Z",
"created_by": "/bin/sh -c set -eux; \t\tarch=\"$(dpkg --print-architecture)\"; \tcase \"$arch\" in \t\t'amd64') \t\t\tdownloadUrl='https://github.com/AdoptOpenJDK/openjdk8-upstream-binaries/releases/download/jdk8u322-b06/OpenJDK8U-jre_x64_linux_8u322b06.tar.gz'; \t\t\t;; \t\t'arm64') \t\t\tdownloadUrl='https://github.com/AdoptOpenJDK/openjdk8-upstream-binaries/releases/download/jdk8u322-b06/OpenJDK8U-jre_aarch64_linux_8u322b06.tar.gz'; \t\t\t;; \t\t*) echo \u003e\u00262 \"error: unsupported architecture: '$arch'\"; exit 1 ;; \tesac; \t\tsavedAptMark=\"$(apt-mark showmanual)\"; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tdirmngr \t\tgnupg \t\twget \t; \trm -rf /var/lib/apt/lists/*; \t\twget --progress=dot:giga -O openjdk.tgz \"$downloadUrl\"; \twget --progress=dot:giga -O openjdk.tgz.asc \"$downloadUrl.sign\"; \t\texport GNUPGHOME=\"$(mktemp -d)\"; \tgpg --batch --keyserver keyserver.ubuntu.com --recv-keys EAC843EBD3EFDB98CC772FADA5CD6035332FA671; \tgpg --batch --keyserver keyserver.ubuntu.com --keyserver-options no-self-sigs-only --recv-keys CA5F11C6CE22644D42C6AC4492EF8D39DC13168F; \tgpg --batch --list-sigs --keyid-format 0xLONG CA5F11C6CE22644D42C6AC4492EF8D39DC13168F \t\t| tee /dev/stderr \t\t| grep '0xA5CD6035332FA671' \t\t| grep 'Andrew Haley'; \tgpg --batch --verify openjdk.tgz.asc openjdk.tgz; \tgpgconf --kill all; \trm -rf \"$GNUPGHOME\"; \t\tmkdir -p \"$JAVA_HOME\"; \ttar --extract \t\t--file openjdk.tgz \t\t--directory \"$JAVA_HOME\" \t\t--strip-components 1 \t\t--no-same-owner \t; \trm openjdk.tgz*; \t\tapt-mark auto '.*' \u003e /dev/null; \t[ -z \"$savedAptMark\" ] || apt-mark manual $savedAptMark \u003e /dev/null; \tapt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \t\t{ \t\techo '#!/usr/bin/env bash'; \t\techo 'set -Eeuo pipefail'; \t\techo 'trust extract --overwrite --format=java-cacerts --filter=ca-anchors --purpose=server-auth \"$JAVA_HOME/lib/security/cacerts\"'; \t} \u003e /etc/ca-certificates/update.d/docker-openjdk; \tchmod +x /etc/ca-certificates/update.d/docker-openjdk; \t/etc/ca-certificates/update.d/docker-openjdk; \t\tfind \"$JAVA_HOME/lib\" -name '*.so' -exec dirname '{}' ';' | sort -u \u003e /etc/ld.so.conf.d/docker-openjdk.conf; \tldconfig; \t\tjava -version"
},
{
"created": "2022-03-30T05:24:58.62001394Z",
"created_by": "/bin/sh -c #(nop) ENV CATALINA_HOME=/usr/local/tomcat",
"empty_layer": true
},
{
"created": "2022-03-30T05:24:58.713370816Z",
"created_by": "/bin/sh -c #(nop) ENV PATH=/usr/local/tomcat/bin:/usr/local/openjdk-8/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"empty_layer": true
},
{
"created": "2022-03-30T05:24:59.23051474Z",
"created_by": "/bin/sh -c mkdir -p \"$CATALINA_HOME\""
},
{
"created": "2022-03-30T05:24:59.327279736Z",
"created_by": "/bin/sh -c #(nop) WORKDIR /usr/local/tomcat",
"empty_layer": true
},
{
"created": "2022-03-30T05:24:59.421537441Z",
"created_by": "/bin/sh -c #(nop) ENV TOMCAT_NATIVE_LIBDIR=/usr/local/tomcat/native-jni-lib",
"empty_layer": true
},
{
"created": "2022-03-30T05:24:59.517366785Z",
"created_by": "/bin/sh -c #(nop) ENV LD_LIBRARY_PATH=/usr/local/tomcat/native-jni-lib",
"empty_layer": true
},
{
"created": "2022-03-30T05:42:09.694872721Z",
"created_by": "/bin/sh -c #(nop) ENV GPG_KEYS=05AB33110949707C93A279E3D3EFE6B686867BA6 07E48665A34DCAFAE522E5E6266191C37C037D42 47309207D818FFD8DCD3F83F1931D684307A10A5 541FBE7D8F78B25E055DDEE13C370389288584E7 5C3C5F3E314C866292F359A8F3AD5C94A67F707E 765908099ACF92702C7D949BFA0C35EA8AA299F1 79F7026C690BAA50B92CD8B66A3AD3F4F22C4FED 9BA44C2621385CB966EBA586F72C284D731FABEE A27677289986DB50844682F8ACB77FC2E86E29AC A9C5DF4D22E99998D9875A5110C01C5A2F6059E7 DCFD35E0BF8CA7344752DE8B6FB21E8933C60243 F3A04C595DB5B6A5F1ECA43E3B7BBB100D811BBE F7DA48BB64BCB84ECBA7EE6935CD23C10D498E23",
"empty_layer": true
},
{
"created": "2022-03-30T05:42:09.790546995Z",
"created_by": "/bin/sh -c #(nop) ENV TOMCAT_MAJOR=8",
"empty_layer": true
},
{
"created": "2022-03-30T05:42:09.882411219Z",
"created_by": "/bin/sh -c #(nop) ENV TOMCAT_VERSION=8.5.77",
"empty_layer": true
},
{
"created": "2022-03-30T05:42:09.972728447Z",
"created_by": "/bin/sh -c #(nop) ENV TOMCAT_SHA512=50f96584cbbbeeda92a3b573e7fe7e2c49e57ed4bc5246257dc1409abac0710b49fa7049a0dd9a3b8467bca2aa078ef608f49b676c1abf12529528ff71bb0260",
"empty_layer": true
},
{
"created": "2022-03-30T05:42:10.425808771Z",
"created_by": "/bin/sh -c #(nop) COPY dir:4b2b2ad5d081ef9a92d92841b7b643214ae6e21bbb961e3197c3615dd08813ab in /usr/local/tomcat "
},
{
"created": "2022-03-30T05:42:13.894391571Z",
"created_by": "/bin/sh -c set -eux; \tapt-get update; \txargs -rt apt-get install -y --no-install-recommends \u003c \"$TOMCAT_NATIVE_LIBDIR/.dependencies.txt\"; \trm -rf /var/lib/apt/lists/*"
},
{
"created": "2022-03-30T05:42:15.098598192Z",
"created_by": "/bin/sh -c set -eux; \tnativeLines=\"$(catalina.sh configtest 2\u003e\u00261)\"; \tnativeLines=\"$(echo \"$nativeLines\" | grep 'Apache Tomcat Native')\"; \tnativeLines=\"$(echo \"$nativeLines\" | sort -u)\"; \tif ! echo \"$nativeLines\" | grep -E 'INFO: Loaded( APR based)? Apache Tomcat Native library' \u003e\u00262; then \t\techo \u003e\u00262 \"$nativeLines\"; \t\texit 1; \tfi"
},
{
"created": "2022-03-30T05:42:15.18926788Z",
"created_by": "/bin/sh -c #(nop) EXPOSE 8080",
"empty_layer": true
},
{
"created": "2022-03-30T05:42:15.283787651Z",
"created_by": "/bin/sh -c #(nop) CMD [\"catalina.sh\" \"run\"]",
"empty_layer": true
},
{
"created": "2022-06-06T13:51:57.120019Z",
"created_by": "/bin/sh -c #(nop) COPY file:4a1136b54136f8775efe918c4cd6af1ad1e507b36a49286d4f2c6bde722d33f4 in /usr/local/tomcat/webapps/ "
}
],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:608f3a074261105f129d707e4d9ad3d41b5baa94887f092b7c2857f7274a2fce",
"sha256:1f6e409d1c59c8e06608a024b82d50490313abc3b2ff93730e43135d5be0cd72",
"sha256:0e78b1e5673e8cc7c102fdda9e6b830b7dee2b29b178f34d25d9be59387e6950",
"sha256:ba40706eccba610401e4942e29f50bdf36807f8638942ce20805b359ae3ac1c1",
"sha256:053db4876c0df3df3294ee00e32e140b130ba33807d088750cb69b0e6fad158e",
"sha256:85595543df2b1115a18284a8ef62d0b235c4bc29e3d33b55f89b54ee1eadf4c6",
"sha256:868d710aa4dc5fc4793508564fc45c991ed8d5f6ab3e4cf52bb856f29546f3d8",
"sha256:77b2d158369254d5055183f5483f8b6661170857b61768d1d95d18c2ec1714b6",
"sha256:eb769943b91f10a0418f2fc3b4a4fde6c6293be60c37293fcc0fa319edaf27a5"
]
},
"config": {
"Cmd": [
"catalina.sh",
"run"
],
"Env": [
"PATH=/usr/local/tomcat/bin:/usr/local/openjdk-8/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"JAVA_HOME=/usr/local/openjdk-8",
"LANG=C.UTF-8",
"JAVA_VERSION=8u322",
"CATALINA_HOME=/usr/local/tomcat",
"TOMCAT_NATIVE_LIBDIR=/usr/local/tomcat/native-jni-lib",
"LD_LIBRARY_PATH=/usr/local/tomcat/native-jni-lib",
"GPG_KEYS=05AB33110949707C93A279E3D3EFE6B686867BA6 07E48665A34DCAFAE522E5E6266191C37C037D42 47309207D818FFD8DCD3F83F1931D684307A10A5 541FBE7D8F78B25E055DDEE13C370389288584E7 5C3C5F3E314C866292F359A8F3AD5C94A67F707E 765908099ACF92702C7D949BFA0C35EA8AA299F1 79F7026C690BAA50B92CD8B66A3AD3F4F22C4FED 9BA44C2621385CB966EBA586F72C284D731FABEE A27677289986DB50844682F8ACB77FC2E86E29AC A9C5DF4D22E99998D9875A5110C01C5A2F6059E7 DCFD35E0BF8CA7344752DE8B6FB21E8933C60243 F3A04C595DB5B6A5F1ECA43E3B7BBB100D811BBE F7DA48BB64BCB84ECBA7EE6935CD23C10D498E23",
"TOMCAT_MAJOR=8",
"TOMCAT_VERSION=8.5.77",
"TOMCAT_SHA512=50f96584cbbbeeda92a3b573e7fe7e2c49e57ed4bc5246257dc1409abac0710b49fa7049a0dd9a3b8467bca2aa078ef608f49b676c1abf12529528ff71bb0260"
],
"Image": "sha256:d69eef03ed55cfa0d799181c477762549e7ce94deb23193f04e6bec0c23d0dfd",
"WorkingDir": "/usr/local/tomcat",
"ExposedPorts": {
"8080/tcp": {}
}
}
}
},
"Results": [
{
"Target": "testdata/fixtures/images/spring4shell-jre8.tar.gz (debian 11.3)",
"Class": "os-pkgs",
"Type": "debian"
},
{
"Target": "Java",
"Class": "lang-pkgs",
"Type": "jar",
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2022-22965",
"PkgName": "org.springframework:spring-beans",
"PkgPath": "usr/local/tomcat/webapps/helloworld.war",
"InstalledVersion": "5.3.15",
"FixedVersion": "5.3.18",
"Layer": {
"Digest": "sha256:cc44af318e91e6f9f9bf73793fa4f0639487613f46aa1f819b02b6e8fb5c6c07",
"DiffID": "sha256:eb769943b91f10a0418f2fc3b4a4fde6c6293be60c37293fcc0fa319edaf27a5"
},
"SeveritySource": "ghsa",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-22965",
"DataSource": {
"ID": "ghsa",
"Name": "GitHub Security Advisory Maven",
"URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven"
},
"Title": "spring-framework: RCE via Data Binding on JDK 9+",
"Description": "A Spring MVC or Spring WebFlux application running on JDK 9+ may be vulnerable to remote code execution (RCE) via data binding. The specific exploit requires the application to run on Tomcat as a WAR deployment. If the application is deployed as a Spring Boot executable jar, i.e. the default, it is not vulnerable to the exploit. However, the nature of the vulnerability is more general, and there may be other ways to exploit it.",
"Severity": "LOW",
"CweIDs": [
"CWE-94"
],
"CVSS": {
"ghsa": {
"V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"V3Score": 9.8
},
"nvd": {
"V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P",
"V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"V2Score": 7.5,
"V3Score": 9.8
},
"redhat": {
"V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
"V3Score": 8.1
}
},
"References": [
"https://github.com/advisories/GHSA-36p3-wjmg-h94x"
]
}
]
},
{
"Target": "",
"Class": "custom",
"CustomResources": [
{
"Type": "spring4shell/java-major-version",
"FilePath": "/usr/local/openjdk-8/release",
"Layer": {
"Digest": "sha256:d7b564a873af313eb2dbcb1ed0d393c57543e3666bdedcbe5d75841d72b1f791",
"DiffID": "sha256:ba40706eccba610401e4942e29f50bdf36807f8638942ce20805b359ae3ac1c1"
},
"Data": "1.8.0_322"
},
{
"Type": "spring4shell/tomcat-version",
"FilePath": "/usr/local/tomcat/RELEASE-NOTES",
"Layer": {
"Digest": "sha256:59c0978ccb117247fd40d936973c40df89195f60466118c5acc6a55f8ba29f06",
"DiffID": "sha256:85595543df2b1115a18284a8ef62d0b235c4bc29e3d33b55f89b54ee1eadf4c6"
},
"Data": "8.5.77"
}
]
}
]
}

View File

@@ -95,6 +95,7 @@ nav:
- AWS CodePipeline: docs/integrations/aws-codepipeline.md
- AWS Security Hub: docs/integrations/aws-security-hub.md
- Advanced:
- Modules: docs/advanced/modules.md
- Plugins: docs/advanced/plugins.md
- Air-Gapped Environment: docs/advanced/air-gap.md
- Container Image:
@@ -120,7 +121,7 @@ nav:
- Repository: docs/references/cli/repo.md
- Client: docs/references/cli/client.md
- Server: docs/references/cli/server.md
- Plugins: docs/references/cli/plugins.md
- Plugin: docs/references/cli/plugin.md
- SBOM: docs/references/cli/sbom.md
- Modes:
- Standalone: docs/references/modes/standalone.md

View File

@@ -328,7 +328,7 @@ func TestRemoteCache_PutArtifactInsecure(t *testing.T) {
imageInfo: types.ArtifactInfo{},
insecure: false,
},
wantErr: "certificate signed by unknown authority",
wantErr: "failed to do request",
},
}
for _, tt := range tests {

View File

@@ -13,6 +13,7 @@ import (
"github.com/aquasecurity/trivy-db/pkg/metadata"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy/pkg/commands/artifact"
"github.com/aquasecurity/trivy/pkg/commands/module"
"github.com/aquasecurity/trivy/pkg/commands/option"
"github.com/aquasecurity/trivy/pkg/commands/plugin"
"github.com/aquasecurity/trivy/pkg/commands/server"
@@ -415,6 +416,7 @@ func NewApp(version string) *cli.App {
NewServerCommand(),
NewConfigCommand(),
NewPluginCommand(),
NewModuleCommand(),
NewK8sCommand(),
NewSbomCommand(),
NewVersionCommand(),
@@ -809,6 +811,31 @@ func NewPluginCommand() *cli.Command {
}
}
// NewModuleCommand is the factory method to add module subcommand
func NewModuleCommand() *cli.Command {
return &cli.Command{
Name: "module",
Aliases: []string{"m"},
Usage: "manage modules",
Subcommands: cli.Commands{
{
Name: "install",
Aliases: []string{"i"},
Usage: "install a module",
ArgsUsage: "REPOSITORY",
Action: module.Install,
},
{
Name: "uninstall",
Aliases: []string{"u"},
Usage: "uninstall a module",
ArgsUsage: "REPOSITORY",
Action: module.Uninstall,
},
},
}
}
// NewK8sCommand is the factory method to add k8s subcommand
func NewK8sCommand() *cli.Command {
k8sSecurityChecksFlag := withValue(

View File

@@ -11,7 +11,6 @@ import (
"github.com/aquasecurity/fanal/artifact"
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/fanal/types"
"github.com/aquasecurity/trivy/pkg/result"
"github.com/aquasecurity/trivy/pkg/rpc/client"
"github.com/aquasecurity/trivy/pkg/scanner"
)
@@ -50,11 +49,6 @@ func initializeRepositoryScanner(ctx context.Context, url string, artifactCache
return scanner.Scanner{}, nil, nil
}
func initializeResultClient() result.Client {
wire.Build(result.SuperSet)
return result.Client{}
}
/////////////////
// Client/Server
/////////////////
@@ -82,8 +76,3 @@ func initializeRemoteFilesystemScanner(ctx context.Context, path string, artifac
wire.Build(scanner.RemoteFilesystemSet)
return scanner.Scanner{}, nil, nil
}
func initializeRemoteResultClient() result.Client {
wire.Build(result.SuperSet)
return result.Client{}
}

View File

@@ -20,7 +20,9 @@ import (
tcache "github.com/aquasecurity/trivy/pkg/cache"
"github.com/aquasecurity/trivy/pkg/commands/operation"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/module"
pkgReport "github.com/aquasecurity/trivy/pkg/report"
"github.com/aquasecurity/trivy/pkg/result"
"github.com/aquasecurity/trivy/pkg/rpc/client"
"github.com/aquasecurity/trivy/pkg/scanner"
"github.com/aquasecurity/trivy/pkg/types"
@@ -78,12 +80,15 @@ type Runner interface {
// Report a writes a report
Report(opt Option, report types.Report) error
// Close closes runner
Close() error
Close(ctx context.Context) error
}
type runner struct {
cache cache.Cache
dbOpen bool
// WASM modules
module *module.Manager
}
type runnerOption func(*runner)
@@ -116,11 +121,19 @@ func NewRunner(cliOption Option, opts ...runnerOption) (Runner, error) {
return nil, xerrors.Errorf("DB error: %w", err)
}
// Initialize WASM modules
m, err := module.NewManager(cliOption.Context.Context)
if err != nil {
return nil, xerrors.Errorf("WASM module error: %w", err)
}
m.Register()
r.module = m
return r, nil
}
// Close closes everything
func (r *runner) Close() error {
func (r *runner) Close(ctx context.Context) error {
var errs error
if err := r.cache.Close(); err != nil {
errs = multierror.Append(errs, err)
@@ -131,6 +144,10 @@ func (r *runner) Close() error {
errs = multierror.Append(errs, err)
}
}
if err := r.module.Close(ctx); err != nil {
errs = multierror.Append(errs, err)
}
return errs
}
@@ -204,14 +221,11 @@ func (r *runner) scanArtifact(ctx context.Context, opt Option, initializeScanner
}
func (r *runner) Filter(ctx context.Context, opt Option, report types.Report) (types.Report, error) {
resultClient := initializeResultClient()
results := report.Results
// Filter results
for i := range results {
// Fill vulnerability info only in standalone mode
if opt.RemoteAddr == "" {
resultClient.FillVulnerabilityInfo(results[i].Vulnerabilities, results[i].Type)
}
vulns, misconfSummary, misconfs, secrets, err := resultClient.Filter(ctx, results[i].Vulnerabilities, results[i].Misconfigurations, results[i].Secrets,
vulns, misconfSummary, misconfs, secrets, err := result.Filter(ctx, results[i].Vulnerabilities, results[i].Misconfigurations, results[i].Secrets,
opt.Severities, opt.IgnoreUnfixed, opt.IncludeNonFailures, opt.IgnoreFile, opt.IgnorePolicy)
if err != nil {
return types.Report{}, xerrors.Errorf("unable to filter vulnerabilities: %w", err)
@@ -324,41 +338,41 @@ func run(ctx context.Context, opt Option, artifactType ArtifactType) (err error)
}
}()
runner, err := NewRunner(opt)
r, err := NewRunner(opt)
if err != nil {
if errors.Is(err, SkipScan) {
return nil
}
return xerrors.Errorf("init error: %w", err)
}
defer runner.Close()
defer r.Close(ctx)
var report types.Report
switch artifactType {
case containerImageArtifact, imageArchiveArtifact:
if report, err = runner.ScanImage(ctx, opt); err != nil {
if report, err = r.ScanImage(ctx, opt); err != nil {
return xerrors.Errorf("image scan error: %w", err)
}
case filesystemArtifact:
if report, err = runner.ScanFilesystem(ctx, opt); err != nil {
if report, err = r.ScanFilesystem(ctx, opt); err != nil {
return xerrors.Errorf("filesystem scan error: %w", err)
}
case rootfsArtifact:
if report, err = runner.ScanRootfs(ctx, opt); err != nil {
if report, err = r.ScanRootfs(ctx, opt); err != nil {
return xerrors.Errorf("rootfs scan error: %w", err)
}
case repositoryArtifact:
if report, err = runner.ScanRepository(ctx, opt); err != nil {
if report, err = r.ScanRepository(ctx, opt); err != nil {
return xerrors.Errorf("repository scan error: %w", err)
}
}
report, err = runner.Filter(ctx, opt, report)
report, err = r.Filter(ctx, opt, report)
if err != nil {
return xerrors.Errorf("filter error: %w", err)
}
if err = runner.Report(opt, report); err != nil {
if err = r.Report(opt, report); err != nil {
return xerrors.Errorf("report error: %w", err)
}

View File

@@ -18,10 +18,10 @@ import (
"github.com/aquasecurity/fanal/types"
"github.com/aquasecurity/trivy-db/pkg/db"
"github.com/aquasecurity/trivy/pkg/detector/ospkg"
"github.com/aquasecurity/trivy/pkg/result"
"github.com/aquasecurity/trivy/pkg/rpc/client"
"github.com/aquasecurity/trivy/pkg/scanner"
"github.com/aquasecurity/trivy/pkg/scanner/local"
"github.com/aquasecurity/trivy/pkg/vulnerability"
)
// Injectors from inject.go:
@@ -31,7 +31,9 @@ import (
func initializeDockerScanner(ctx context.Context, imageName string, artifactCache cache.ArtifactCache, localArtifactCache cache.LocalArtifactCache, dockerOpt types.DockerOption, artifactOption artifact.Option) (scanner.Scanner, func(), error) {
applierApplier := applier.NewApplier(localArtifactCache)
detector := ospkg.Detector{}
localScanner := local.NewScanner(applierApplier, detector)
config := db.Config{}
client := vulnerability.NewClient(config)
localScanner := local.NewScanner(applierApplier, detector, client)
typesImage, cleanup, err := image.NewContainerImage(ctx, imageName, dockerOpt)
if err != nil {
return scanner.Scanner{}, nil, err
@@ -52,7 +54,9 @@ func initializeDockerScanner(ctx context.Context, imageName string, artifactCach
func initializeArchiveScanner(ctx context.Context, filePath string, artifactCache cache.ArtifactCache, localArtifactCache cache.LocalArtifactCache, artifactOption artifact.Option) (scanner.Scanner, error) {
applierApplier := applier.NewApplier(localArtifactCache)
detector := ospkg.Detector{}
localScanner := local.NewScanner(applierApplier, detector)
config := db.Config{}
client := vulnerability.NewClient(config)
localScanner := local.NewScanner(applierApplier, detector, client)
typesImage, err := image.NewArchiveImage(filePath)
if err != nil {
return scanner.Scanner{}, err
@@ -69,7 +73,9 @@ func initializeArchiveScanner(ctx context.Context, filePath string, artifactCach
func initializeFilesystemScanner(ctx context.Context, path string, artifactCache cache.ArtifactCache, localArtifactCache cache.LocalArtifactCache, artifactOption artifact.Option) (scanner.Scanner, func(), error) {
applierApplier := applier.NewApplier(localArtifactCache)
detector := ospkg.Detector{}
localScanner := local.NewScanner(applierApplier, detector)
config := db.Config{}
client := vulnerability.NewClient(config)
localScanner := local.NewScanner(applierApplier, detector, client)
artifactArtifact, err := local2.NewArtifact(path, artifactCache, artifactOption)
if err != nil {
return scanner.Scanner{}, nil, err
@@ -82,7 +88,9 @@ func initializeFilesystemScanner(ctx context.Context, path string, artifactCache
func initializeRepositoryScanner(ctx context.Context, url string, artifactCache cache.ArtifactCache, localArtifactCache cache.LocalArtifactCache, artifactOption artifact.Option) (scanner.Scanner, func(), error) {
applierApplier := applier.NewApplier(localArtifactCache)
detector := ospkg.Detector{}
localScanner := local.NewScanner(applierApplier, detector)
config := db.Config{}
client := vulnerability.NewClient(config)
localScanner := local.NewScanner(applierApplier, detector, client)
artifactArtifact, cleanup, err := remote.NewArtifact(url, artifactCache, artifactOption)
if err != nil {
return scanner.Scanner{}, nil, err
@@ -93,12 +101,6 @@ func initializeRepositoryScanner(ctx context.Context, url string, artifactCache
}, nil
}
func initializeResultClient() result.Client {
config := db.Config{}
client := result.NewClient(config)
return client
}
// initializeRemoteDockerScanner is for container image scanning in client/server mode
// e.g. dockerd, container registry, podman, etc.
func initializeRemoteDockerScanner(ctx context.Context, imageName string, artifactCache cache.ArtifactCache, remoteScanOptions client.ScannerOption, dockerOpt types.DockerOption, artifactOption artifact.Option) (scanner.Scanner, func(), error) {
@@ -152,9 +154,3 @@ func initializeRemoteFilesystemScanner(ctx context.Context, path string, artifac
return scannerScanner, func() {
}, nil
}
func initializeRemoteResultClient() result.Client {
config := db.Config{}
resultClient := result.NewClient(config)
return resultClient
}

View File

@@ -0,0 +1,58 @@
package module
import (
"github.com/urfave/cli/v2"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/commands/option"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/module"
)
// Install installs a module
func Install(c *cli.Context) error {
if c.NArg() != 1 {
cli.ShowSubcommandHelpAndExit(c, 1)
}
if err := initLogger(c); err != nil {
return xerrors.Errorf("log initialization error: %w", err)
}
repo := c.Args().First()
if err := module.Install(c.Context, repo, c.Bool("quiet"), c.Bool("insecure")); err != nil {
return xerrors.Errorf("module installation error: %w", err)
}
return nil
}
// Uninstall uninstalls a module
func Uninstall(c *cli.Context) error {
if c.NArg() != 1 {
cli.ShowSubcommandHelpAndExit(c, 1)
}
if err := initLogger(c); err != nil {
return xerrors.Errorf("log initialization error: %w", err)
}
repo := c.Args().First()
if err := module.Uninstall(c.Context, repo); err != nil {
return xerrors.Errorf("module uninstall error: %w", err)
}
return nil
}
func initLogger(ctx *cli.Context) error {
conf, err := option.NewGlobalOption(ctx)
if err != nil {
return xerrors.Errorf("config error: %w", err)
}
if err = log.InitLogger(conf.Debug, conf.Quiet); err != nil {
return xerrors.Errorf("failed to initialize a logger: %w", err)
}
return nil
}

View File

@@ -114,7 +114,7 @@ func (c *ReportOption) populateVulnTypes() error {
}
for _, v := range strings.Split(c.vulnType, ",") {
if types.NewVulnType(v) == types.VulnTypeUnknown {
if !slices.Contains(types.VulnTypes, v) {
return xerrors.Errorf("unknown vulnerability type (%s)", v)
}
c.VulnType = append(c.VulnType, v)
@@ -128,7 +128,7 @@ func (c *ReportOption) populateSecurityChecks() error {
}
for _, v := range strings.Split(c.securityChecks, ",") {
if types.NewSecurityCheck(v) == types.SecurityCheckUnknown {
if !slices.Contains(types.SecurityChecks, v) {
return xerrors.Errorf("unknown security check (%s)", v)
}
c.SecurityChecks = append(c.SecurityChecks, v)

View File

@@ -7,6 +7,7 @@ import (
"github.com/aquasecurity/trivy-db/pkg/db"
"github.com/aquasecurity/trivy/pkg/commands/operation"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/module"
rpcServer "github.com/aquasecurity/trivy/pkg/rpc/server"
"github.com/aquasecurity/trivy/pkg/utils"
)
@@ -52,6 +53,13 @@ func run(c Option) (err error) {
return xerrors.Errorf("error in vulnerability DB initialize: %w", err)
}
// Initialize WASM modules
m, err := module.NewManager(c.Context.Context)
if err != nil {
return xerrors.Errorf("WASM module error: %w", err)
}
m.Register()
server := rpcServer.NewServer(c.AppVersion, c.Listen, c.CacheDir, c.Token, c.TokenHeader)
return server.ListenAndServe(cache, c.Insecure)
}

View File

@@ -6,7 +6,7 @@ import (
"testing"
"time"
"github.com/google/go-containerregistry/pkg/v1"
v1 "github.com/google/go-containerregistry/pkg/v1"
fakei "github.com/google/go-containerregistry/pkg/v1/fake"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/google/go-containerregistry/pkg/v1/types"
@@ -201,6 +201,20 @@ func TestClient_Download(t *testing.T) {
// Mock image
img := new(fakei.FakeImage)
img.LayersReturns([]v1.Layer{newFakeLayer(t, tt.input)}, nil)
img.ManifestReturns(&v1.Manifest{
Layers: []v1.Descriptor{
{
MediaType: "application/vnd.aquasec.trivy.db.layer.v1.tar+gzip",
Size: 100,
Digest: v1.Hash{
Algorithm: "sha256",
Hex: "aec482bc254b5dd025d3eaf5bb35997d3dba783e394e8f91d5a415963151bfb8"},
Annotations: map[string]string{
"org.opencontainers.image.title": "db.tar.gz",
},
},
},
}, nil)
// Mock OCI artifact
art, err := oci.NewArtifact("db", mediaType, true, false, oci.WithImage(img))

View File

@@ -3,7 +3,6 @@ package ospkg
import (
"time"
"github.com/google/wire"
"golang.org/x/xerrors"
fos "github.com/aquasecurity/fanal/analyzer/os"
@@ -27,12 +26,6 @@ var (
// ErrUnsupportedOS defines error for unsupported OS
ErrUnsupportedOS = xerrors.New("unsupported os")
// SuperSet binds dependencies for OS scan
SuperSet = wire.NewSet(
wire.Struct(new(Detector)),
wire.Bind(new(Operation), new(Detector)),
)
drivers = map[string]Driver{
fos.Alpine: alpine.NewScanner(),
fos.Alma: alma.NewScanner(),
@@ -55,11 +48,6 @@ func RegisterDriver(name string, driver Driver) {
drivers[name] = driver
}
// Operation defines operation of OSpkg scan
type Operation interface {
Detect(string, string, string, *ftypes.Repository, time.Time, []ftypes.Package) ([]types.DetectedVulnerability, bool, error)
}
// Driver defines operations for OS package scan
type Driver interface {
Detect(string, *ftypes.Repository, []ftypes.Package) ([]types.DetectedVulnerability, error)

View File

@@ -1,52 +0,0 @@
package ospkg
import (
"time"
ftypes "github.com/aquasecurity/fanal/types"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/stretchr/testify/mock"
)
type MockDetector struct {
mock.Mock
}
type DetectInput struct {
ImageName string
OSFamily string
OSName string
Created time.Time
Pkgs []ftypes.Package
}
type DetectOutput struct {
Vulns []types.DetectedVulnerability
Eosl bool
Err error
}
type DetectExpectation struct {
Args DetectInput
ReturnArgs DetectOutput
}
func NewMockDetector(detectExpectations []DetectExpectation) *MockDetector {
mockDetector := new(MockDetector)
for _, e := range detectExpectations {
mockDetector.On("Detect", e.Args.ImageName, e.Args.OSFamily, e.Args.OSName, e.Args.Created, e.Args.Pkgs).Return(
e.ReturnArgs.Vulns, e.ReturnArgs.Eosl, e.ReturnArgs.Err)
}
return mockDetector
}
func (_m *MockDetector) Detect(a string, b string, c string, d time.Time, e []ftypes.Package) ([]types.DetectedVulnerability, bool, error) {
ret := _m.Called(a, b, c, d, e)
ret0 := ret.Get(0)
if ret0 == nil {
return nil, false, ret.Error(2)
}
vulns, ok := ret0.([]types.DetectedVulnerability)
if !ok {
return nil, false, ret.Error(2)
}
return vulns, ret.Bool(1), ret.Error(2)
}

View File

@@ -62,7 +62,7 @@ func run(ctx context.Context, opt cmd.Option, cluster string, artifacts []*artif
return xerrors.Errorf("init error: %w", err)
}
defer func() {
if err := runner.Close(); err != nil {
if err := runner.Close(ctx); err != nil {
log.Logger.Errorf("failed to close runner: %s", err)
}
}()

26
pkg/module/api/api.go Normal file
View File

@@ -0,0 +1,26 @@
package api
import "github.com/aquasecurity/trivy/pkg/module/serialize"
const (
Version = 1
ActionInsert serialize.PostScanAction = "INSERT"
ActionUpdate serialize.PostScanAction = "UPDATE"
ActionDelete serialize.PostScanAction = "DELETE"
)
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)
}

54
pkg/module/command.go Normal file
View File

@@ -0,0 +1,54 @@
package module
import (
"context"
"os"
"path/filepath"
"github.com/google/go-containerregistry/pkg/name"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/oci"
)
const mediaType = "application/vnd.module.wasm.content.layer.v1+wasm"
// Install installs a module
func Install(ctx context.Context, repo string, quiet, insecure bool) error {
ref, err := name.ParseReference(repo)
if err != nil {
return xerrors.Errorf("repository parse error: %w", err)
}
log.Logger.Infof("Installing the module from %s...", repo)
artifact, err := oci.NewArtifact(repo, mediaType, quiet, insecure)
if err != nil {
return xerrors.Errorf("module initialize error: %w", err)
}
dst := filepath.Join(dir(), ref.Context().Name())
log.Logger.Debugf("Installing the module to %s...", dst)
if err = artifact.Download(ctx, dst); err != nil {
return xerrors.Errorf("module download error: %w", err)
}
return nil
}
// Uninstall uninstalls a module
func Uninstall(_ context.Context, repo string) error {
ref, err := name.ParseReference(repo)
if err != nil {
return xerrors.Errorf("repository parse error: %w", err)
}
log.Logger.Infof("Uninstalling %s ...", repo)
dst := filepath.Join(dir(), ref.Context().Name())
if err = os.RemoveAll(dst); err != nil {
return xerrors.Errorf("remove error: %w", err)
}
return nil
}

715
pkg/module/module.go Normal file
View File

@@ -0,0 +1,715 @@
package module
import (
"context"
"encoding/json"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
"github.com/hashicorp/go-multierror"
"github.com/liamg/memoryfs"
"github.com/mailru/easyjson"
"github.com/samber/lo"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
wasi "github.com/tetratelabs/wazero/wasi_snapshot_preview1"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/log"
tapi "github.com/aquasecurity/trivy/pkg/module/api"
"github.com/aquasecurity/trivy/pkg/module/serialize"
"github.com/aquasecurity/trivy/pkg/scanner/post"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/aquasecurity/trivy/pkg/utils"
)
var (
exportFunctions = map[string]interface{}{
"debug": logDebug,
"info": logInfo,
"warn": logWarn,
"error": logError,
}
RelativeDir = filepath.Join(".trivy", "modules")
)
func logDebug(ctx context.Context, m api.Module, offset, size uint32) {
buf := readMemory(ctx, m, offset, size)
if buf != nil {
log.Logger.Debug(string(buf))
}
}
func logInfo(ctx context.Context, m api.Module, offset, size uint32) {
buf := readMemory(ctx, m, offset, size)
if buf != nil {
log.Logger.Info(string(buf))
}
}
func logWarn(ctx context.Context, m api.Module, offset, size uint32) {
buf := readMemory(ctx, m, offset, size)
if buf != nil {
log.Logger.Warn(string(buf))
}
}
func logError(ctx context.Context, m api.Module, offset, size uint32) {
buf := readMemory(ctx, m, offset, size)
if buf != nil {
log.Logger.Error(string(buf))
}
}
func readMemory(ctx context.Context, m api.Module, offset, size uint32) []byte {
buf, ok := m.Memory().Read(ctx, offset, size)
if !ok {
log.Logger.Errorf("Memory.Read(%d, %d) out of range", offset, size)
return nil
}
return buf
}
type Manager struct {
runtime wazero.Runtime
modules []*wasmModule
}
func NewManager(ctx context.Context) (*Manager, error) {
m := &Manager{}
// Create a new WebAssembly Runtime.
m.runtime = wazero.NewRuntime()
// Load WASM modules in local
if err := m.loadModules(ctx); err != nil {
return nil, xerrors.Errorf("module load error: %w", err)
}
return m, nil
}
func (m *Manager) loadModules(ctx context.Context) error {
moduleDir := dir()
_, err := os.Stat(moduleDir)
if os.IsNotExist(err) {
return nil
}
log.Logger.Debugf("Module dir: %s", moduleDir)
err = filepath.Walk(moduleDir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
} else if info.IsDir() || filepath.Ext(info.Name()) != ".wasm" {
return nil
}
rel, err := filepath.Rel(moduleDir, path)
if err != nil {
return xerrors.Errorf("failed to get a relative path: %w", err)
}
log.Logger.Infof("Loading %s...", rel)
wasmCode, err := os.ReadFile(path)
if err != nil {
return xerrors.Errorf("file read error: %w", err)
}
p, err := newWASMPlugin(ctx, m.runtime, wasmCode)
if err != nil {
return xerrors.Errorf("WASM module init error %s: %w", rel, err)
}
m.modules = append(m.modules, p)
return nil
})
if err != nil {
return xerrors.Errorf("module walk error: %w", err)
}
return nil
}
func (m *Manager) Register() {
for _, mod := range m.modules {
mod.Register()
}
}
func (m *Manager) Close(ctx context.Context) error {
var errs error
if err := m.runtime.Close(ctx); err != nil {
errs = multierror.Append(errs, err)
}
for _, p := range m.modules {
if err := p.Close(ctx); err != nil {
errs = multierror.Append(errs, err)
}
}
return errs
}
func splitPtrSize(u uint64) (uint32, uint32) {
ptr := uint32(u >> 32)
size := uint32(u)
return ptr, size
}
func ptrSizeToString(ctx context.Context, m api.Module, ptrSize uint64) (string, error) {
ptr, size := splitPtrSize(ptrSize)
buf := readMemory(ctx, m, ptr, size)
if buf == nil {
return "", xerrors.New("unable to read memory")
}
return string(buf), nil
}
// stringToPtr returns a pointer and size pair for the given string in a way compatible with WebAssembly numeric types.
func stringToPtrSize(ctx context.Context, s string, mod api.Module, malloc api.Function) (uint64, uint64, error) {
size := uint64(len(s))
results, err := malloc.Call(ctx, size)
if err != nil {
return 0, 0, xerrors.Errorf("malloc error: %w", err)
}
// The pointer is a linear memory offset, which is where we write the string.
ptr := results[0]
if !mod.Memory().Write(ctx, uint32(ptr), []byte(s)) {
return 0, 0, xerrors.Errorf("Memory.Write(%d, %d) out of range of memory size %d",
ptr, size, mod.Memory().Size(ctx))
}
return ptr, size, nil
}
func unmarshal(ctx context.Context, m api.Module, ptrSize uint64, v any) error {
ptr, size := splitPtrSize(ptrSize)
buf := readMemory(ctx, m, ptr, size)
if buf == nil {
return xerrors.New("unable to read memory")
}
if err := json.Unmarshal(buf, v); err != nil {
return xerrors.Errorf("unmarshal error: %w", err)
}
return nil
}
func marshal(ctx context.Context, m api.Module, malloc api.Function, v easyjson.Marshaler) (uint64, uint64, error) {
b, err := easyjson.Marshal(v)
if err != nil {
return 0, 0, xerrors.Errorf("marshal error: %w", err)
}
size := uint64(len(b))
results, err := malloc.Call(ctx, size)
if err != nil {
return 0, 0, xerrors.Errorf("malloc error: %w", err)
}
// The pointer is a linear memory offset, which is where we write the marshaled value.
ptr := results[0]
if !m.Memory().Write(ctx, uint32(ptr), b) {
return 0, 0, xerrors.Errorf("Memory.Write(%d, %d) out of range of memory size %d",
ptr, size, m.Memory().Size(ctx))
}
return ptr, size, nil
}
type wasmModule struct {
mod api.Module
name string
version int
requiredFiles []*regexp.Regexp
isAnalyzer bool
isPostScanner bool
postScanSpec serialize.PostScanSpec
// Exported functions
analyze api.Function
postScan api.Function
malloc api.Function // TinyGo specific
free api.Function // TinyGo specific
}
func newWASMPlugin(ctx context.Context, r wazero.Runtime, code []byte) (*wasmModule, error) {
// Combine the above into our baseline config, overriding defaults (which discard stdout and have no file system).
config := wazero.NewModuleConfig().WithStdout(os.Stdout).WithFS(memoryfs.New())
// Create an empty namespace so that multiple modules will not conflict
ns := r.NewNamespace(ctx)
// Instantiate a Go-defined module named "env" that exports functions.
_, err := r.NewModuleBuilder("env").
ExportMemory("mem", 100).
ExportFunctions(exportFunctions).
Instantiate(ctx, ns)
if err != nil {
return nil, xerrors.Errorf("wasm module build error: %w", err)
}
if _, err = wasi.NewBuilder(r).Instantiate(ctx, ns); err != nil {
return nil, xerrors.Errorf("WASI init error: %w", err)
}
// Compile the WebAssembly module using the default configuration.
compiled, err := r.CompileModule(ctx, code, wazero.NewCompileConfig())
if err != nil {
return nil, xerrors.Errorf("module compile error: %w", err)
}
// InstantiateModule runs the "_start" function which is what TinyGo compiles "main" to.
mod, err := ns.InstantiateModule(ctx, compiled, config)
if err != nil {
return nil, xerrors.Errorf("module init error: %w", err)
}
// These are undocumented, but exported. See tinygo-org/tinygo#2788
// TODO: improve TinyGo specific code
malloc := mod.ExportedFunction("malloc")
free := mod.ExportedFunction("free")
// Get a module name
name, err := moduleName(ctx, mod)
if err != nil {
return nil, xerrors.Errorf("failed to get a module name: %w", err)
}
// Get a module version
version, err := moduleVersion(ctx, mod)
if err != nil {
return nil, xerrors.Errorf("failed to get a module version: %w", err)
}
// Get a module API version
apiVersion, err := moduleAPIVersion(ctx, mod)
if err != nil {
return nil, xerrors.Errorf("failed to get a module version: %w", err)
}
if apiVersion != tapi.Version {
log.Logger.Infof("Ignore %s@v%d module due to API version mismatch, got: %d, want: %d",
name, version, apiVersion, tapi.Version)
return nil, nil
}
isAnalyzer, err := moduleIsAnalyzer(ctx, mod)
if err != nil {
return nil, xerrors.Errorf("failed to check if the module is an analyzer: %w", err)
}
isPostScanner, err := moduleIsPostScanner(ctx, mod)
if err != nil {
return nil, xerrors.Errorf("failed to check if the module is a post scanner: %w", err)
}
// Get exported functions by WASM module
analyzeFunc := mod.ExportedFunction("analyze")
if analyzeFunc == nil {
return nil, xerrors.New("analyze() must be exported")
}
postScanFunc := mod.ExportedFunction("post_scan")
if postScanFunc == nil {
return nil, xerrors.New("post_scan() must be exported")
}
var requiredFiles []*regexp.Regexp
if isAnalyzer {
// Get required files
requiredFiles, err = moduleRequiredFiles(ctx, mod)
if err != nil {
return nil, xerrors.Errorf("failed to get required files: %w", err)
}
}
var postScanSpec serialize.PostScanSpec
if isPostScanner {
// This spec defines how the module works in post scanning like INSERT, UPDATE and DELETE.
postScanSpec, err = modulePostScanSpec(ctx, mod)
if err != nil {
return nil, xerrors.Errorf("failed to get a post scan spec: %w", err)
}
}
return &wasmModule{
mod: mod,
name: name,
version: version,
requiredFiles: requiredFiles,
isAnalyzer: isAnalyzer,
isPostScanner: isPostScanner,
postScanSpec: postScanSpec,
analyze: analyzeFunc,
postScan: postScanFunc,
malloc: malloc,
free: free,
}, nil
}
func (m *wasmModule) Register() {
log.Logger.Infof("Registering WASM module: %s@v%d", m.name, m.version)
if m.isAnalyzer {
log.Logger.Debugf("Registering custom analyzer in %s@v%d", m.name, m.version)
analyzer.RegisterAnalyzer(m)
}
if m.isPostScanner {
log.Logger.Debugf("Registering custom post scanner in %s@v%d", m.name, m.version)
post.RegisterPostScanner(m)
}
}
func (m *wasmModule) Close(ctx context.Context) error {
return m.mod.Close(ctx)
}
func (m *wasmModule) Type() analyzer.Type {
return analyzer.Type(m.name)
}
func (m *wasmModule) Name() string {
return m.name
}
func (m *wasmModule) Version() int {
return m.version
}
func (m *wasmModule) Required(filePath string, _ os.FileInfo) bool {
for _, r := range m.requiredFiles {
if r.MatchString(filePath) {
return true
}
}
return false
}
func (m *wasmModule) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
filePath := "/" + filepath.ToSlash(input.FilePath)
log.Logger.Debugf("Module %s: analyzing %s...", m.name, filePath)
memfs := memoryfs.New()
if err := memfs.MkdirAll(filepath.Dir(filePath), fs.ModePerm); err != nil {
return nil, xerrors.Errorf("memory fs mkdir error: %w", err)
}
err := memfs.WriteLazyFile(filePath, func() (io.Reader, error) {
return input.Content, nil
}, fs.ModePerm)
if err != nil {
return nil, xerrors.Errorf("memory fs write error: %w", err)
}
// Pass memory fs to the analyze() function
ctx, closer, err := experimental.WithFS(ctx, memfs)
if err != nil {
return nil, xerrors.Errorf("fs error: %w", err)
}
defer closer.Close(ctx)
inputPtr, inputSize, err := stringToPtrSize(ctx, filePath, m.mod, m.malloc)
if err != nil {
return nil, xerrors.Errorf("failed to write string to memory: %w", err)
}
defer m.free.Call(ctx, inputPtr) // nolint: errcheck
analyzeRes, err := m.analyze.Call(ctx, inputPtr, inputSize)
if err != nil {
return nil, xerrors.Errorf("analyze error: %w", err)
} else if len(analyzeRes) != 1 {
return nil, xerrors.New("invalid signature: analyze")
}
var result analyzer.AnalysisResult
if err = unmarshal(ctx, m.mod, analyzeRes[0], &result); err != nil {
return nil, xerrors.Errorf("invalid return value: %w", err)
}
return &result, nil
}
// PostScan performs post scanning
// e.g. Remove a vulnerability, change severity, etc.
func (m *wasmModule) PostScan(ctx context.Context, results types.Results) (types.Results, error) {
// Find custom resources
var custom serialize.Result
for _, result := range results {
if result.Class == types.ClassCustom {
custom = serialize.Result(result)
break
}
}
arg := serialize.Results{custom}
switch m.postScanSpec.Action {
case tapi.ActionUpdate, tapi.ActionDelete:
// Pass the relevant results to the module
arg = append(arg, findIDs(m.postScanSpec.IDs, results)...)
}
// Marshal the argument into WASM memory so that the WASM module can read it.
inputPtr, inputSize, err := marshal(ctx, m.mod, m.malloc, arg)
if err != nil {
return nil, xerrors.Errorf("post scan marshal error: %w", err)
}
defer m.free.Call(ctx, inputPtr) //nolint: errcheck
analyzeRes, err := m.postScan.Call(ctx, inputPtr, inputSize)
if err != nil {
return nil, xerrors.Errorf("post scan invocation error: %w", err)
} else if len(analyzeRes) != 1 {
return nil, xerrors.New("invalid signature: post_scan")
}
var got types.Results
if err = unmarshal(ctx, m.mod, analyzeRes[0], &got); err != nil {
return nil, xerrors.Errorf("post scan unmarshal error: %w", err)
}
switch m.postScanSpec.Action {
case tapi.ActionInsert:
results = append(results, lo.Filter(got, func(r types.Result, _ int) bool {
return r.Class != types.ClassCustom
})...)
case tapi.ActionUpdate:
updateResults(got, results)
case tapi.ActionDelete:
deleteResults(got, results)
}
return results, nil
}
func findIDs(ids []string, results types.Results) serialize.Results {
var filtered serialize.Results
for _, result := range results {
if result.Class == types.ClassCustom {
continue
}
vulns := lo.Filter(result.Vulnerabilities, func(v types.DetectedVulnerability, _ int) bool {
return slices.Contains(ids, v.VulnerabilityID)
})
misconfs := lo.Filter(result.Misconfigurations, func(m types.DetectedMisconfiguration, _ int) bool {
return slices.Contains(ids, m.ID)
})
if len(vulns) > 0 || len(misconfs) > 0 {
filtered = append(filtered, serialize.Result{
Target: result.Target,
Class: result.Class,
Type: result.Type,
Vulnerabilities: vulns,
Misconfigurations: misconfs,
})
}
}
return filtered
}
func updateResults(gotResults, results types.Results) {
for _, g := range gotResults {
for i, result := range results {
if g.Target == result.Target && g.Class == result.Class && g.Type == result.Type {
results[i].Vulnerabilities = lo.Map(result.Vulnerabilities, func(v types.DetectedVulnerability, _ int) types.DetectedVulnerability {
// Update vulnerabilities in the existing result
for _, got := range g.Vulnerabilities {
if got.VulnerabilityID == v.VulnerabilityID && got.PkgName == v.PkgName &&
got.PkgPath == v.PkgPath && got.InstalledVersion == v.InstalledVersion {
// Override vulnerability details
v.SeveritySource = got.SeveritySource
v.Vulnerability = got.Vulnerability
}
}
return v
})
results[i].Misconfigurations = lo.Map(result.Misconfigurations, func(m types.DetectedMisconfiguration, _ int) types.DetectedMisconfiguration {
// Update misconfigurations in the existing result
for _, got := range g.Misconfigurations {
if got.ID == m.ID &&
got.CauseMetadata.StartLine == m.CauseMetadata.StartLine &&
got.CauseMetadata.EndLine == m.CauseMetadata.EndLine {
// Override misconfiguration details
m.CauseMetadata = got.CauseMetadata
m.Severity = got.Severity
m.Status = got.Status
}
}
return m
})
}
}
}
}
func deleteResults(gotResults, results types.Results) {
for _, gotResult := range gotResults {
for i, result := range results {
// Remove vulnerabilities and misconfigurations from the existing result
if gotResult.Target == result.Target && gotResult.Class == result.Class && gotResult.Type == result.Type {
results[i].Vulnerabilities = lo.Reject(result.Vulnerabilities, func(v types.DetectedVulnerability, _ int) bool {
for _, got := range gotResult.Vulnerabilities {
if got.VulnerabilityID == v.VulnerabilityID && got.PkgName == v.PkgName &&
got.PkgPath == v.PkgPath && got.InstalledVersion == v.InstalledVersion {
return true
}
}
return false
})
results[i].Misconfigurations = lo.Reject(result.Misconfigurations, func(v types.DetectedMisconfiguration, _ int) bool {
for _, got := range gotResult.Misconfigurations {
if got.ID == v.ID && got.Status == v.Status &&
got.CauseMetadata.StartLine == v.CauseMetadata.StartLine &&
got.CauseMetadata.EndLine == v.CauseMetadata.EndLine {
return true
}
}
return false
})
}
}
}
}
func moduleName(ctx context.Context, mod api.Module) (string, error) {
nameFunc := mod.ExportedFunction("name")
if nameFunc == nil {
return "", xerrors.New("name() must be exported")
}
nameRes, err := nameFunc.Call(ctx)
if err != nil {
return "", xerrors.Errorf("wasm function name() invocation error: %w", err)
} else if len(nameRes) != 1 {
return "", xerrors.New("invalid signature: name()")
}
name, err := ptrSizeToString(ctx, mod, nameRes[0])
if err != nil {
return "", xerrors.Errorf("invalid return value: %w", err)
}
return name, nil
}
func moduleVersion(ctx context.Context, mod api.Module) (int, error) {
versionFunc := mod.ExportedFunction("version")
if versionFunc == nil {
return 0, xerrors.New("version() must be exported")
}
versionRes, err := versionFunc.Call(ctx)
if err != nil {
return 0, xerrors.Errorf("wasm function version() invocation error: %w", err)
} else if len(versionRes) != 1 {
return 0, xerrors.New("invalid signature: version")
}
return int(versionRes[0]), nil
}
func moduleAPIVersion(ctx context.Context, mod api.Module) (int, error) {
versionFunc := mod.ExportedFunction("api_version")
if versionFunc == nil {
return 0, xerrors.New("api_version() must be exported")
}
versionRes, err := versionFunc.Call(ctx)
if err != nil {
return 0, xerrors.Errorf("wasm function api_version() invocation error: %w", err)
} else if len(versionRes) != 1 {
return 0, xerrors.New("invalid signature: api_version")
}
return int(versionRes[0]), nil
}
func moduleRequiredFiles(ctx context.Context, mod api.Module) ([]*regexp.Regexp, error) {
requiredFilesFunc := mod.ExportedFunction("required")
if requiredFilesFunc == nil {
return nil, xerrors.New("required() must be exported")
}
requiredFilesRes, err := requiredFilesFunc.Call(ctx)
if err != nil {
return nil, xerrors.Errorf("wasm function required() invocation error: %w", err)
} else if len(requiredFilesRes) != 1 {
return nil, xerrors.New("invalid signature: required_files")
}
var fileRegexps serialize.StringSlice
if err = unmarshal(ctx, mod, requiredFilesRes[0], &fileRegexps); err != nil {
return nil, xerrors.Errorf("invalid return value: %w", err)
}
var requiredFiles []*regexp.Regexp
for _, file := range fileRegexps {
re, err := regexp.Compile(file)
if err != nil {
return nil, xerrors.Errorf("regexp compile error: %w", err)
}
requiredFiles = append(requiredFiles, re)
}
return requiredFiles, nil
}
func moduleIsAnalyzer(ctx context.Context, mod api.Module) (bool, error) {
return isType(ctx, mod, "is_analyzer")
}
func moduleIsPostScanner(ctx context.Context, mod api.Module) (bool, error) {
return isType(ctx, mod, "is_post_scanner")
}
func isType(ctx context.Context, mod api.Module, name string) (bool, error) {
isFunc := mod.ExportedFunction(name)
if isFunc == nil {
return false, xerrors.Errorf("%s() must be exported", name)
}
isRes, err := isFunc.Call(ctx)
if err != nil {
return false, xerrors.Errorf("wasm function %s() invocation error: %w", name, err)
} else if len(isRes) != 1 {
return false, xerrors.Errorf("invalid signature: %s", name)
}
return isRes[0] > 0, nil
}
func dir() string {
return filepath.Join(utils.HomeDir(), RelativeDir)
}
func modulePostScanSpec(ctx context.Context, mod api.Module) (serialize.PostScanSpec, error) {
postScanSpecFunc := mod.ExportedFunction("post_scan_spec")
if postScanSpecFunc == nil {
return serialize.PostScanSpec{}, xerrors.New("post_scan_spec() must be exported")
}
postScanSpecRes, err := postScanSpecFunc.Call(ctx)
if err != nil {
return serialize.PostScanSpec{}, xerrors.Errorf("wasm function post_scan_spec() invocation error: %w", err)
} else if len(postScanSpecRes) != 1 {
return serialize.PostScanSpec{}, xerrors.New("invalid signature: post_scan_spec")
}
var spec serialize.PostScanSpec
if err = unmarshal(ctx, mod, postScanSpecRes[0], &spec); err != nil {
return serialize.PostScanSpec{}, xerrors.Errorf("invalid return value: %w", err)
}
return spec, nil
}

103
pkg/module/module_test.go Normal file
View File

@@ -0,0 +1,103 @@
package module_test
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/module"
"github.com/aquasecurity/trivy/pkg/scanner/post"
"github.com/aquasecurity/trivy/pkg/utils"
)
func TestManager_Register(t *testing.T) {
tests := []struct {
name string
noModuleDir bool
moduleName string
wantAnalyzerVersions map[string]int
wantPostScannerVersions map[string]int
wantErr bool
}{
{
name: "happy path",
moduleName: "happy",
wantAnalyzerVersions: map[string]int{
"happy": 1,
},
wantPostScannerVersions: map[string]int{
"happy": 1,
},
},
{
name: "only analyzer",
moduleName: "analyzer",
wantAnalyzerVersions: map[string]int{
"analyzer": 1,
},
wantPostScannerVersions: map[string]int{},
},
{
name: "only post scanner",
moduleName: "scanner",
wantAnalyzerVersions: map[string]int{},
wantPostScannerVersions: map[string]int{
"scanner": 2,
},
},
{
name: "no module dir",
noModuleDir: true,
moduleName: "happy",
wantAnalyzerVersions: map[string]int{},
wantPostScannerVersions: map[string]int{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
modulePath := filepath.Join("testdata", tt.moduleName, tt.moduleName+".wasm")
// WASM modules must be generated before running this test.
if _, err := os.Stat(modulePath); os.IsNotExist(err) {
require.Fail(t, "missing WASM modules, try 'make test' or 'make generate-test-modules'")
}
// Set up a temp dir for modules
tmpDir := t.TempDir()
t.Setenv("XDG_DATA_HOME", tmpDir)
moduleDir := filepath.Join(tmpDir, module.RelativeDir)
if !tt.noModuleDir {
err := os.MkdirAll(moduleDir, 0777)
require.NoError(t, err)
// Copy the wasm module for testing
_, err = utils.CopyFile(modulePath, filepath.Join(moduleDir, filepath.Base(modulePath)))
require.NoError(t, err)
}
m, err := module.NewManager(context.Background())
require.NoError(t, err)
// Register analyzer and post scanner from WASM module
m.Register()
defer func() {
analyzer.DeregisterAnalyzer(analyzer.Type(tt.moduleName))
post.DeregisterPostScanner(tt.moduleName)
}()
// Confirm the analyzer is registered
got := analyzer.NewAnalyzerGroup("", nil).AnalyzerVersions()
assert.Equal(t, tt.wantAnalyzerVersions, got)
// Confirm the post scanner is registered
got = post.ScannerVersions()
assert.Equal(t, tt.wantPostScannerVersions, got)
})
}
}

View File

@@ -0,0 +1,52 @@
package serialize
import (
"github.com/aquasecurity/trivy/pkg/types"
)
// TinyGo doesn't support encoding/json, but github.com/mailru/easyjson for now.
// We need to generate JSON-related methods like MarshalEasyJSON implementing easyjson.Marshaler.
//
// $ make easyjson
//easyjson:json
type StringSlice []string
//easyjson:json
type AnalysisResult struct {
// TODO: support other fields as well
//OS *types.OS
//Repository *types.Repository
//PackageInfos []types.PackageInfo
//Applications []types.Application
//Secrets []types.Secret
//SystemInstalledFiles []string // A list of files installed by OS package manager
// Currently it supports custom resources only
CustomResources []CustomResource
}
type CustomResource struct {
Type string
FilePath string
Data interface{}
}
type PostScanAction string
//easyjson:json
type PostScanSpec struct {
// What action the module will do in post scanning.
// value: INSERT, UPDATE and DELETE
Action PostScanAction
// IDs represent which vulnerability and misconfiguration ID will be updated or deleted in post scanning.
// When the action is UPDATE, the matched result will be passed to the module.
IDs []string
}
//easyjson:json
type Results []Result
//easyjson:json
type Result types.Result

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
//go:generate tinygo build -o analyzer.wasm -scheduler=none -target=wasi --no-debug analyzer.go
//go:build tinygo.wasm
package main
import (
"github.com/aquasecurity/trivy/pkg/module/serialize"
"github.com/aquasecurity/trivy/pkg/module/wasm"
)
const (
moduleVersion = 1
moduleName = "analyzer"
)
func main() {
wasm.RegisterModule(AnalyzerModule{})
}
type AnalyzerModule struct{}
func (AnalyzerModule) Version() int {
return moduleVersion
}
func (AnalyzerModule) Name() string {
return moduleName
}
func (AnalyzerModule) RequiredFiles() []string {
return []string{
`foo(.?)`,
}
}
func (s AnalyzerModule) Analyze(_ string) (*serialize.AnalysisResult, error) {
return nil, nil
}

47
pkg/module/testdata/happy/happy.go vendored Normal file
View File

@@ -0,0 +1,47 @@
//go:generate tinygo build -o happy.wasm -scheduler=none -target=wasi --no-debug happy.go
//go:build tinygo.wasm
package main
import (
"github.com/aquasecurity/trivy/pkg/module/api"
"github.com/aquasecurity/trivy/pkg/module/serialize"
"github.com/aquasecurity/trivy/pkg/module/wasm"
)
const (
moduleVersion = 1
moduleName = "happy"
)
func main() {
wasm.RegisterModule(HappyModule{})
}
type HappyModule struct{}
func (HappyModule) Version() int {
return moduleVersion
}
func (HappyModule) Name() string {
return moduleName
}
func (HappyModule) RequiredFiles() []string {
return []string{}
}
func (s HappyModule) Analyze(_ string) (*serialize.AnalysisResult, error) {
return nil, nil
}
func (HappyModule) PostScanSpec() serialize.PostScanSpec {
return serialize.PostScanSpec{
Action: api.ActionInsert, // Add new vulnerabilities
}
}
func (HappyModule) PostScan(_ serialize.Results) (serialize.Results, error) {
return nil, nil
}

39
pkg/module/testdata/scanner/scanner.go vendored Normal file
View File

@@ -0,0 +1,39 @@
//go:generate tinygo build -o scanner.wasm -scheduler=none -target=wasi --no-debug scanner.go
//go:build tinygo.wasm
package main
import (
"github.com/aquasecurity/trivy/pkg/module/api"
"github.com/aquasecurity/trivy/pkg/module/serialize"
"github.com/aquasecurity/trivy/pkg/module/wasm"
)
const (
moduleVersion = 2
moduleName = "scanner"
)
func main() {
wasm.RegisterModule(PostScannerModule{})
}
type PostScannerModule struct{}
func (PostScannerModule) Version() int {
return moduleVersion
}
func (PostScannerModule) Name() string {
return moduleName
}
func (PostScannerModule) PostScanSpec() serialize.PostScanSpec {
return serialize.PostScanSpec{
Action: api.ActionInsert, // Add new vulnerabilities
}
}
func (PostScannerModule) PostScan(_ serialize.Results) (serialize.Results, error) {
return nil, nil
}

179
pkg/module/wasm/sdk.go Normal file
View File

@@ -0,0 +1,179 @@
//go:build tinygo.wasm
package wasm
// This package is designed to be imported by WASM modules.
// TinyGo can build this package, but Go cannot.
import (
"fmt"
"reflect"
"unsafe"
"github.com/mailru/easyjson"
"github.com/aquasecurity/trivy/pkg/module/api"
"github.com/aquasecurity/trivy/pkg/module/serialize"
)
func Debug(message string) {
message = fmt.Sprintf("Module %s: %s", module.Name(), message)
ptr, size := stringToPtr(message)
_debug(ptr, size)
}
func Info(message string) {
message = fmt.Sprintf("Module %s: %s", module.Name(), message)
ptr, size := stringToPtr(message)
_info(ptr, size)
}
func Warn(message string) {
message = fmt.Sprintf("Module %s: %s", module.Name(), message)
ptr, size := stringToPtr(message)
_warn(ptr, size)
}
func Error(message string) {
message = fmt.Sprintf("Module %s: %s", module.Name(), message)
ptr, size := stringToPtr(message)
_error(ptr, size)
}
//go:wasm-module env
//export debug
func _debug(ptr uint32, size uint32)
//go:wasm-module env
//export info
func _info(ptr uint32, size uint32)
//go:wasm-module env
//export warn
func _warn(ptr uint32, size uint32)
//go:wasm-module env
//export error
func _error(ptr uint32, size uint32)
var module api.Module
func RegisterModule(p api.Module) {
module = p
}
//export name
func _name() uint64 {
name := module.Name()
ptr, size := stringToPtr(name)
return (uint64(ptr) << uint64(32)) | uint64(size)
}
//export api_version
func _apiVersion() uint32 {
return api.Version
}
//export version
func _version() uint32 {
return uint32(module.Version())
}
//export is_analyzer
func _isAnalyzer() uint64 {
if _, ok := module.(api.Analyzer); !ok {
return 0
}
return 1
}
//export required
func _required() uint64 {
files := module.(api.Analyzer).RequiredFiles()
ss := serialize.StringSlice(files)
return marshal(ss)
}
//export analyze
func _analyze(ptr, size uint32) uint64 {
filePath := ptrToString(ptr, size)
custom, err := module.(api.Analyzer).Analyze(filePath)
if err != nil {
Error(fmt.Sprintf("analyze error: %s", err))
return 0
}
return marshal(custom)
}
//export is_post_scanner
func _isPostScanner() uint64 {
if _, ok := module.(api.PostScanner); !ok {
return 0
}
return 1
}
//export post_scan_spec
func _post_scan_spec() uint64 {
return marshal(module.(api.PostScanner).PostScanSpec())
}
//export post_scan
func _post_scan(ptr, size uint32) uint64 {
var results serialize.Results
if err := unmarshal(ptr, size, &results); err != nil {
Error(fmt.Sprintf("post scan error: %s", err))
return 0
}
results, err := module.(api.PostScanner).PostScan(results)
if err != nil {
Error(fmt.Sprintf("post scan error: %s", err))
return 0
}
return marshal(results)
}
func marshal(v easyjson.Marshaler) uint64 {
b, err := easyjson.Marshal(v)
if err != nil {
Error(fmt.Sprintf("marshal error: %s", err))
return 0
}
p := uintptr(unsafe.Pointer(&b[0]))
return (uint64(p) << uint64(32)) | uint64(len(b))
}
func unmarshal(ptr, size uint32, v easyjson.Unmarshaler) error {
var b []byte
s := (*reflect.SliceHeader)(unsafe.Pointer(&b))
s.Len = uintptr(size)
s.Cap = uintptr(size)
s.Data = uintptr(ptr)
if err := easyjson.Unmarshal(b, v); err != nil {
return fmt.Errorf("unmarshal error: %s", err)
}
return nil
}
// ptrToString returns a string from WebAssembly compatible numeric types representing its pointer and length.
func ptrToString(ptr uint32, size uint32) string {
// Get a slice view of the underlying bytes in the stream. We use SliceHeader, not StringHeader
// as it allows us to fix the capacity to what was allocated.
return *(*string)(unsafe.Pointer(&reflect.SliceHeader{
Data: uintptr(ptr),
Len: uintptr(size), // Tinygo requires these as uintptrs even if they are int fields.
Cap: uintptr(size), // ^^ See https://github.com/tinygo-org/tinygo/issues/1284
}))
}
// stringToPtr returns a pointer and size pair for the given string in a way compatible with WebAssembly numeric types.
func stringToPtr(s string) (uint32, uint32) {
buf := []byte(s)
ptr := &buf[0]
unsafePtr := uintptr(unsafe.Pointer(ptr))
return uint32(unsafePtr), uint32(len(buf))
}

View File

@@ -6,9 +6,10 @@ import (
"io"
"net/http"
"os"
"path/filepath"
"github.com/cheggaaa/pb/v3"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
@@ -17,6 +18,8 @@ import (
"github.com/aquasecurity/trivy/pkg/downloader"
)
const titleAnnotation = "org.opencontainers.image.title"
type options struct {
img v1.Image
}
@@ -33,6 +36,7 @@ func WithImage(img v1.Image) Option {
// Artifact is used to download artifacts such as vulnerability database and policies from OCI registries.
type Artifact struct {
fileName string
image v1.Image
layer v1.Layer // Take the first layer as OCI artifact
quiet bool
@@ -52,7 +56,7 @@ func NewArtifact(repo, mediaType string, quiet, insecure bool, opts ...Option) (
return nil, xerrors.Errorf("repository name error (%s): %w", repo, err)
}
var remoteOpts []remote.Option
remoteOpts := []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain)}
if insecure {
t := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
@@ -71,14 +75,25 @@ func NewArtifact(repo, mediaType string, quiet, insecure bool, opts ...Option) (
return nil, xerrors.Errorf("OCI layer error: %w", err)
}
manifest, err := o.img.Manifest()
if err != nil {
return nil, xerrors.Errorf("OCI manifest error: %w", err)
}
// A single layer is only supported now.
if len(layers) != 1 {
if len(layers) != 1 || len(manifest.Layers) != 1 {
return nil, xerrors.Errorf("OCI artifact must be a single layer")
}
// Take the first layer
layer := layers[0]
// Take the file name of the first layer
fileName, ok := manifest.Layers[0].Annotations[titleAnnotation]
if !ok {
return nil, xerrors.Errorf("annotation %s is missing", titleAnnotation)
}
layerMediaType, err := layer.MediaType()
if err != nil {
return nil, xerrors.Errorf("media type error: %w", err)
@@ -87,6 +102,7 @@ func NewArtifact(repo, mediaType string, quiet, insecure bool, opts ...Option) (
}
return &Artifact{
fileName: fileName,
image: o.img,
layer: layer,
quiet: quiet,
@@ -114,13 +130,18 @@ func (a Artifact) Download(ctx context.Context, dir string) error {
defer bar.Finish()
// https://github.com/hashicorp/go-getter/issues/326
f, err := os.CreateTemp("", "artifact-*.tar.gz")
tempDir, err := os.MkdirTemp("", "trivy")
if err != nil {
return xerrors.Errorf("failed to create a temp dir: %w", err)
}
f, err := os.Create(filepath.Join(tempDir, a.fileName))
if err != nil {
return xerrors.Errorf("failed to create a temp file: %w", err)
}
defer func() {
_ = f.Close()
_ = os.Remove(f.Name())
_ = os.RemoveAll(tempDir)
}()
// Download the layer content into a temporal file
@@ -128,7 +149,7 @@ func (a Artifact) Download(ctx context.Context, dir string) error {
return xerrors.Errorf("copy error: %w", err)
}
// Decompress artifact-xxx.tar.gz and copy it into the cache dir
// Decompress the downloaded file if it is compressed and copy it into the dst
if err = downloader.Download(ctx, f.Name(), dir, dir); err != nil {
return xerrors.Errorf("download error: %w", err)
}

View File

@@ -82,6 +82,20 @@ func TestNewArtifact(t *testing.T) {
// Mock image
img := new(fakei.FakeImage)
img.LayersReturns(tt.layersReturns.layers, tt.layersReturns.err)
img.ManifestReturns(&v1.Manifest{
Layers: []v1.Descriptor{
{
MediaType: "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip",
Size: 100,
Digest: v1.Hash{
Algorithm: "sha256",
Hex: "cba33656188782852f58993f45b68bfb8577f64cdcf02a604e3fc2afbeb5f2d8"},
Annotations: map[string]string{
"org.opencontainers.image.title": "bundle.tar.gz",
},
},
},
}, nil)
_, err = oci.NewArtifact("repo", tt.mediaType, true, false, oci.WithImage(img))
if tt.wantErr != "" {
@@ -126,6 +140,20 @@ func TestArtifact_Download(t *testing.T) {
// Mock image
img := new(fakei.FakeImage)
img.LayersReturns([]v1.Layer{flayer}, nil)
img.ManifestReturns(&v1.Manifest{
Layers: []v1.Descriptor{
{
MediaType: "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip",
Size: 100,
Digest: v1.Hash{
Algorithm: "sha256",
Hex: "cba33656188782852f58993f45b68bfb8577f64cdcf02a604e3fc2afbeb5f2d8"},
Annotations: map[string]string{
"org.opencontainers.image.title": "bundle.tar.gz",
},
},
},
}, nil)
mediaType := "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip"
artifact, err := oci.NewArtifact("repo", mediaType, true, false, oci.WithImage(img))

View File

@@ -19,7 +19,6 @@ import (
const (
configFile = "plugin.yaml"
xdgDataHome = "XDG_DATA_HOME"
)
var (
@@ -297,13 +296,7 @@ func loadMetadata(dir string) (Plugin, error) {
}
func dir() string {
dataHome := os.Getenv(xdgDataHome)
if dataHome != "" {
return filepath.Join(dataHome, pluginsRelativeDir)
}
homeDir, _ := os.UserHomeDir()
return filepath.Join(homeDir, pluginsRelativeDir)
return filepath.Join(utils.HomeDir(), pluginsRelativeDir)
}
func isInstalled(url string) (Plugin, bool) {

View File

@@ -228,16 +228,16 @@ func TestWriter_Write(t *testing.T) {
PackageLicenseDeclared: "GPLv2+",
IsFilesAnalyzedTagPresent: true,
},
spdx.ElementID("ee8bb4e8354184d"): {
PackageSPDXIdentifier: spdx.ElementID("ee8bb4e8354184d"),
spdx.ElementID("216407676208fcb1"): {
PackageSPDXIdentifier: spdx.ElementID("216407676208fcb1"),
PackageName: "actionpack",
PackageVersion: "7.0.1",
PackageLicenseConcluded: "NONE",
PackageLicenseDeclared: "NONE",
IsFilesAnalyzedTagPresent: true,
},
spdx.ElementID("216407676208fcb1"): {
PackageSPDXIdentifier: spdx.ElementID("216407676208fcb1"),
spdx.ElementID("ee8bb4e8354184d"): {
PackageSPDXIdentifier: spdx.ElementID("ee8bb4e8354184d"),
PackageName: "actionpack",
PackageVersion: "7.0.1",
PackageLicenseConcluded: "NONE",

View File

@@ -45,6 +45,10 @@ type TableWriter struct {
// Write writes the result on standard output
func (tw TableWriter) Write(report types.Report) error {
for _, result := range report.Results {
// Not display a table of custom resources
if result.Class == types.ClassCustom {
continue
}
tw.write(result)
}
return nil

View File

@@ -8,16 +8,13 @@ import (
"sort"
"strings"
"github.com/google/wire"
"github.com/open-policy-agent/opa/rego"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
ftypes "github.com/aquasecurity/fanal/types"
"github.com/aquasecurity/trivy-db/pkg/db"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/types"
)
@@ -27,112 +24,8 @@ const (
DefaultIgnoreFile = ".trivyignore"
)
var (
primaryURLPrefixes = map[dbTypes.SourceID][]string{
vulnerability.Debian: {"http://www.debian.org", "https://www.debian.org"},
vulnerability.Ubuntu: {"http://www.ubuntu.com", "https://usn.ubuntu.com"},
vulnerability.RedHat: {"https://access.redhat.com"},
vulnerability.SuseCVRF: {"http://lists.opensuse.org", "https://lists.opensuse.org"},
vulnerability.OracleOVAL: {"http://linux.oracle.com/errata", "https://linux.oracle.com/errata"},
vulnerability.NodejsSecurityWg: {"https://www.npmjs.com", "https://hackerone.com"},
vulnerability.RubySec: {"https://groups.google.com"},
}
)
// SuperSet binds the dependencies
var SuperSet = wire.NewSet(
wire.Struct(new(db.Config)),
NewClient,
)
// Client implements db operations
type Client struct {
dbc db.Operation
}
// NewClient is the factory method for Client
func NewClient(dbc db.Config) Client {
return Client{dbc: dbc}
}
// FillVulnerabilityInfo fills extra info in vulnerability objects
func (c Client) FillVulnerabilityInfo(vulns []types.DetectedVulnerability, reportType string) {
for i := range vulns {
vulnID := vulns[i].VulnerabilityID
vuln, err := c.dbc.GetVulnerability(vulnID)
if err != nil {
log.Logger.Warnf("Error while getting vulnerability details: %s\n", err)
continue
}
// Detect the data source
var source dbTypes.SourceID
if vulns[i].DataSource != nil {
source = vulns[i].DataSource.ID
}
// Select the severity according to the detected source.
severity, severitySource := c.getVendorSeverity(&vuln, source)
// The vendor might provide package-specific severity like Debian.
// For example, CVE-2015-2328 in Debian has "unimportant" for mongodb and "low" for pcre3.
// In that case, we keep the severity as is.
if vulns[i].SeveritySource != "" {
severity = vulns[i].Severity
severitySource = vulns[i].SeveritySource
}
// Add the vulnerability detail
vulns[i].Vulnerability = vuln
vulns[i].Severity = severity
vulns[i].SeveritySource = severitySource
vulns[i].PrimaryURL = c.getPrimaryURL(vulnID, vuln.References, source)
}
}
func (c Client) getVendorSeverity(vuln *dbTypes.Vulnerability, source dbTypes.SourceID) (string, dbTypes.SourceID) {
if vs, ok := vuln.VendorSeverity[source]; ok {
return vs.String(), source
}
// Try NVD as a fallback if it exists
if vs, ok := vuln.VendorSeverity[vulnerability.NVD]; ok {
return vs.String(), vulnerability.NVD
}
if vuln.Severity == "" {
return dbTypes.SeverityUnknown.String(), ""
}
return vuln.Severity, ""
}
func (c Client) getPrimaryURL(vulnID string, refs []string, source dbTypes.SourceID) string {
switch {
case strings.HasPrefix(vulnID, "CVE-"):
return "https://avd.aquasec.com/nvd/" + strings.ToLower(vulnID)
case strings.HasPrefix(vulnID, "RUSTSEC-"):
return "https://osv.dev/vulnerability/" + vulnID
case strings.HasPrefix(vulnID, "GHSA-"):
return "https://github.com/advisories/" + vulnID
case strings.HasPrefix(vulnID, "TEMP-"):
return "https://security-tracker.debian.org/tracker/" + vulnID
}
prefixes := primaryURLPrefixes[source]
for _, pre := range prefixes {
for _, ref := range refs {
if strings.HasPrefix(ref, pre) {
return ref
}
}
}
return ""
}
// Filter filter out the vulnerabilities
func (c Client) Filter(ctx context.Context, vulns []types.DetectedVulnerability, misconfs []types.DetectedMisconfiguration, secrets []ftypes.SecretFinding,
// Filter filters out the vulnerabilities
func Filter(ctx context.Context, vulns []types.DetectedVulnerability, misconfs []types.DetectedMisconfiguration, secrets []ftypes.SecretFinding,
severities []dbTypes.Severity, ignoreUnfixed, includeNonFailures bool, ignoreFile, policyFile string) (
[]types.DetectedVulnerability, *types.MisconfSummary, []types.DetectedMisconfiguration, []ftypes.SecretFinding, error) {
ignoredIDs := getIgnoredIDs(ignoreFile)

View File

@@ -1,4 +1,4 @@
package result
package result_test
import (
"context"
@@ -7,358 +7,12 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
fos "github.com/aquasecurity/fanal/analyzer/os"
ftypes "github.com/aquasecurity/fanal/types"
"github.com/aquasecurity/trivy-db/pkg/db"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy-db/pkg/utils"
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
"github.com/aquasecurity/trivy/pkg/dbtest"
"github.com/aquasecurity/trivy/pkg/result"
"github.com/aquasecurity/trivy/pkg/types"
)
func TestClient_FillVulnerabilityInfo(t *testing.T) {
type args struct {
vulns []types.DetectedVulnerability
reportType string
}
tests := []struct {
name string
fixtures []string
args args
expectedVulnerabilities []types.DetectedVulnerability
}{
{
name: "happy path, with only OS vulnerability but no vendor severity, no NVD",
fixtures: []string{"testdata/fixtures/vulnerability.yaml"},
args: args{
vulns: []types.DetectedVulnerability{
{VulnerabilityID: "CVE-2019-0001"},
},
reportType: fos.RedHat,
},
expectedVulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0001",
Vulnerability: dbTypes.Vulnerability{
Title: "dos",
Description: "dos vulnerability",
Severity: dbTypes.SeverityMedium.String(),
References: []string{"http://example.com"},
LastModifiedDate: utils.MustTimeParse("2020-01-01T01:01:00Z"),
PublishedDate: utils.MustTimeParse("2001-01-01T01:01:00Z"),
},
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2019-0001",
},
},
},
{
name: "happy path, with only OS vulnerability but no vendor severity, yes NVD",
fixtures: []string{"testdata/fixtures/vulnerability.yaml"},
args: args{
vulns: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0002",
DataSource: &dbTypes.DataSource{
ID: vulnerability.Ubuntu,
Name: "Ubuntu CVE Tracker",
URL: "https://git.launchpad.net/ubuntu-cve-tracker",
},
},
},
reportType: fos.Ubuntu,
},
expectedVulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0002",
Vulnerability: dbTypes.Vulnerability{
Title: "dos",
Description: "dos vulnerability",
Severity: dbTypes.SeverityLow.String(),
VendorSeverity: dbTypes.VendorSeverity{
vulnerability.NVD: dbTypes.SeverityLow,
},
References: []string{"http://example.com"},
LastModifiedDate: utils.MustTimeParse("2020-01-01T01:01:00Z"),
PublishedDate: utils.MustTimeParse("2001-01-01T01:01:00Z"),
},
SeveritySource: vulnerability.NVD,
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2019-0002",
DataSource: &dbTypes.DataSource{
ID: vulnerability.Ubuntu,
Name: "Ubuntu CVE Tracker",
URL: "https://git.launchpad.net/ubuntu-cve-tracker",
},
},
},
},
{
name: "happy path, with only OS vulnerability but no severity, no vendor severity, no NVD",
fixtures: []string{"testdata/fixtures/vulnerability.yaml"},
args: args{
vulns: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0003",
DataSource: &dbTypes.DataSource{
ID: vulnerability.Ubuntu,
Name: "Ubuntu CVE Tracker",
URL: "https://git.launchpad.net/ubuntu-cve-tracker",
},
},
},
reportType: fos.Ubuntu,
},
expectedVulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0003",
Vulnerability: dbTypes.Vulnerability{
Title: "dos",
Description: "dos vulnerability",
Severity: dbTypes.SeverityUnknown.String(),
References: []string{"http://example.com"},
},
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2019-0003",
DataSource: &dbTypes.DataSource{
ID: vulnerability.Ubuntu,
Name: "Ubuntu CVE Tracker",
URL: "https://git.launchpad.net/ubuntu-cve-tracker",
},
},
},
},
{
name: "happy path, with only OS vulnerability, yes vendor severity, with both NVD and CVSS info",
fixtures: []string{"testdata/fixtures/vulnerability.yaml"},
args: args{
vulns: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0004",
DataSource: &dbTypes.DataSource{
ID: vulnerability.RedHat,
Name: "Red Hat OVAL v2",
URL: "https://www.redhat.com/security/data/oval/v2/",
},
},
},
reportType: fos.CentOS,
},
expectedVulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0004",
Vulnerability: dbTypes.Vulnerability{
Title: "dos",
Description: "dos vulnerability",
Severity: dbTypes.SeverityLow.String(),
VendorSeverity: dbTypes.VendorSeverity{
vulnerability.RedHat: dbTypes.SeverityLow,
},
CweIDs: []string{"CWE-311"},
References: []string{"http://example.com"},
CVSS: map[dbTypes.SourceID]dbTypes.CVSS{
vulnerability.NVD: {
V2Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P",
V2Score: 4.5,
V3Vector: "CVSS:3.0/PR:N/UI:N/S:U/C:H/I:H/A:H",
V3Score: 5.6,
},
vulnerability.RedHat: {
V2Vector: "AV:N/AC:M/Au:N/C:N/I:P/A:N",
V2Score: 7.8,
V3Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
V3Score: 9.8,
},
},
},
SeveritySource: vulnerability.RedHat,
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2019-0004",
DataSource: &dbTypes.DataSource{
ID: vulnerability.RedHat,
Name: "Red Hat OVAL v2",
URL: "https://www.redhat.com/security/data/oval/v2/",
},
},
},
},
{
name: "happy path, with only library vulnerability",
fixtures: []string{"testdata/fixtures/vulnerability.yaml"},
args: args{
vulns: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0005",
DataSource: &dbTypes.DataSource{
ID: vulnerability.GHSA,
Name: "GitHub Security Advisory Pip",
URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip",
},
},
},
reportType: ftypes.Poetry,
},
expectedVulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0005",
Vulnerability: dbTypes.Vulnerability{
Title: "COVID-19",
Description: "a nasty virus vulnerability for humans",
Severity: dbTypes.SeverityCritical.String(),
VendorSeverity: dbTypes.VendorSeverity{
vulnerability.GHSA: dbTypes.SeverityCritical,
},
References: []string{"https://www.who.int/emergencies/diseases/novel-coronavirus-2019"},
},
SeveritySource: vulnerability.GHSA,
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2019-0005",
DataSource: &dbTypes.DataSource{
ID: vulnerability.GHSA,
Name: "GitHub Security Advisory Pip",
URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip",
},
},
},
},
{
name: "happy path, with package-specific severity",
fixtures: []string{"testdata/fixtures/vulnerability.yaml"},
args: args{
vulns: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0001",
SeveritySource: vulnerability.Debian,
Vulnerability: dbTypes.Vulnerability{
Severity: dbTypes.SeverityLow.String(),
},
},
},
reportType: fos.Debian,
},
expectedVulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0001",
SeveritySource: vulnerability.Debian,
Vulnerability: dbTypes.Vulnerability{
Title: "dos",
Description: "dos vulnerability",
Severity: dbTypes.SeverityLow.String(),
References: []string{"http://example.com"},
LastModifiedDate: utils.MustTimeParse("2020-01-01T01:01:00Z"),
PublishedDate: utils.MustTimeParse("2001-01-01T01:01:00Z"),
},
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2019-0001",
},
},
},
{
name: "GetVulnerability returns an error",
fixtures: []string{"testdata/fixtures/sad.yaml"},
args: args{
vulns: []types.DetectedVulnerability{
{VulnerabilityID: "CVE-2019-0004"},
},
},
expectedVulnerabilities: []types.DetectedVulnerability{
{VulnerabilityID: "CVE-2019-0004"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dbtest.InitDB(t, tt.fixtures)
defer db.Close()
c := Client{
dbc: db.Config{},
}
c.FillVulnerabilityInfo(tt.args.vulns, tt.args.reportType)
assert.Equal(t, tt.expectedVulnerabilities, tt.args.vulns, tt.name)
})
}
}
func TestClient_getPrimaryURL(t *testing.T) {
type args struct {
vulnID string
refs []string
source dbTypes.SourceID
}
tests := []struct {
name string
args args
want string
}{
{
name: "CVE-ID",
args: args{
vulnID: "CVE-2014-8484",
refs: []string{"http://linux.oracle.com/cve/CVE-2014-8484.html"},
source: vulnerability.OracleOVAL,
},
want: "https://avd.aquasec.com/nvd/cve-2014-8484",
},
{
name: "RUSTSEC",
args: args{
vulnID: "RUSTSEC-2018-0017",
refs: []string{"https://github.com/rust-lang-deprecated/tempdir/pull/46"},
source: vulnerability.OSV,
},
want: "https://osv.dev/vulnerability/RUSTSEC-2018-0017",
},
{
name: "GHSA",
args: args{
vulnID: "GHSA-28fw-88hq-6jmm",
refs: []string{},
source: vulnerability.PhpSecurityAdvisories,
},
want: "https://github.com/advisories/GHSA-28fw-88hq-6jmm",
},
{
name: "Debian temp vulnerability",
args: args{
vulnID: "TEMP-0841856-B18BAF",
refs: []string{},
source: vulnerability.Debian,
},
want: "https://security-tracker.debian.org/tracker/TEMP-0841856-B18BAF",
},
{
name: "npm",
args: args{
vulnID: "NSWG-ECO-516",
refs: []string{
"https://hackerone.com/reports/712065",
"https://github.com/lodash/lodash/pull/4759",
"https://www.npmjs.com/advisories/1523",
},
source: vulnerability.NodejsSecurityWg,
},
want: "https://www.npmjs.com/advisories/1523",
},
{
name: "suse",
args: args{
vulnID: "openSUSE-SU-2019:2596-1",
refs: []string{
"http://lists.opensuse.org/opensuse-security-announce/2019-11/msg00076.html",
"https://www.suse.com/support/security/rating/",
},
source: vulnerability.SuseCVRF,
},
want: "http://lists.opensuse.org/opensuse-security-announce/2019-11/msg00076.html",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := Client{}
got := c.getPrimaryURL(tt.args.vulnID, tt.args.refs, tt.args.source)
assert.Equal(t, tt.want, got)
})
}
}
func TestClient_Filter(t *testing.T) {
type args struct {
vulns []types.DetectedVulnerability
@@ -799,8 +453,7 @@ func TestClient_Filter(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := Client{}
gotVulns, gotMisconfSummary, gotMisconfs, gotSecrets, err := c.Filter(context.Background(), tt.args.vulns, tt.args.misconfs, tt.args.secrets,
gotVulns, gotMisconfSummary, gotMisconfs, gotSecrets, err := result.Filter(context.Background(), tt.args.vulns, tt.args.misconfs, tt.args.secrets,
tt.args.severities, tt.args.ignoreUnfixed, false, tt.args.ignoreFile, tt.args.policyFile)
require.NoError(t, err)
assert.Equal(t, tt.wantVulns, gotVulns)

View File

@@ -61,8 +61,8 @@ func NewScanner(scannerOptions ScannerOption, opts ...Option) Scanner {
}
// Scan scans the image
func (s Scanner) Scan(target, artifactKey string, blobKeys []string, options types.ScanOptions) (types.Results, *ftypes.OS, error) {
ctx := WithCustomHeaders(context.Background(), s.customHeaders)
func (s Scanner) Scan(ctx context.Context, target, artifactKey string, blobKeys []string, options types.ScanOptions) (types.Results, *ftypes.OS, error) {
ctx = WithCustomHeaders(ctx, s.customHeaders)
var res *rpc.ScanResponse
err := r.Retry(func() error {

View File

@@ -1,6 +1,7 @@
package client
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
@@ -197,7 +198,7 @@ func TestScanner_Scan(t *testing.T) {
s := NewScanner(ScannerOption{CustomHeaders: tt.customHeaders}, WithRPCClient(client))
gotResults, gotOS, err := s.Scan(tt.args.target, tt.args.imageID, tt.args.layerIDs, tt.args.options)
gotResults, gotOS, err := s.Scan(context.Background(), tt.args.target, tt.args.imageID, tt.args.layerIDs, tt.args.options)
if tt.wantErr != "" {
require.NotNil(t, err, tt.name)
@@ -228,7 +229,7 @@ func TestScanner_ScanServerInsecure(t *testing.T) {
{
name: "sad path",
insecure: false,
wantErr: "certificate signed by unknown authority",
wantErr: "failed to do request",
},
}
for _, tt := range tests {
@@ -241,7 +242,7 @@ func TestScanner_ScanServerInsecure(t *testing.T) {
},
})
s := NewScanner(ScannerOption{Insecure: tt.insecure}, WithRPCClient(c))
_, _, err := s.Scan("dummy", "", nil, types.ScanOptions{})
_, _, err := s.Scan(context.Background(), "dummy", "", nil, types.ScanOptions{})
if tt.wantErr != "" {
require.Error(t, err)

View File

@@ -8,7 +8,6 @@ import (
"golang.org/x/xerrors"
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/trivy/pkg/result"
"github.com/aquasecurity/trivy/pkg/rpc"
"github.com/aquasecurity/trivy/pkg/scanner"
"github.com/aquasecurity/trivy/pkg/scanner/local"
@@ -21,36 +20,31 @@ import (
var ScanSuperSet = wire.NewSet(
local.SuperSet,
wire.Bind(new(scanner.Driver), new(local.Scanner)),
result.SuperSet,
NewScanServer,
)
// ScanServer implements the scanner
type ScanServer struct {
localScanner scanner.Driver
resultClient result.Client
}
// NewScanServer is the factory method for scanner
func NewScanServer(s scanner.Driver, vulnClient result.Client) *ScanServer {
return &ScanServer{localScanner: s, resultClient: vulnClient}
func NewScanServer(s scanner.Driver) *ScanServer {
return &ScanServer{localScanner: s}
}
// Scan scans and return response
func (s *ScanServer) Scan(_ context.Context, in *rpcScanner.ScanRequest) (*rpcScanner.ScanResponse, error) {
func (s *ScanServer) Scan(ctx context.Context, in *rpcScanner.ScanRequest) (*rpcScanner.ScanResponse, error) {
options := types.ScanOptions{
VulnType: in.Options.VulnType,
SecurityChecks: in.Options.SecurityChecks,
ListAllPackages: in.Options.ListAllPackages,
}
results, os, err := s.localScanner.Scan(in.Target, in.ArtifactId, in.BlobIds, options)
results, os, err := s.localScanner.Scan(ctx, in.Target, in.ArtifactId, in.BlobIds, options)
if err != nil {
return nil, xerrors.Errorf("failed scan, %s: %w", in.Target, err)
}
for i := range results {
s.resultClient.FillVulnerabilityInfo(results[i].Vulnerabilities, results[i].Type)
}
return rpc.ConvertToRPCScanResponse(results, os), nil
}

View File

@@ -15,12 +15,9 @@ import (
"github.com/aquasecurity/fanal/cache"
ftypes "github.com/aquasecurity/fanal/types"
"github.com/aquasecurity/trivy-db/pkg/db"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy-db/pkg/utils"
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
"github.com/aquasecurity/trivy/pkg/dbtest"
"github.com/aquasecurity/trivy/pkg/result"
"github.com/aquasecurity/trivy/pkg/scanner"
"github.com/aquasecurity/trivy/pkg/types"
rpcCache "github.com/aquasecurity/trivy/rpc/cache"
@@ -39,7 +36,6 @@ func TestScanServer_Scan(t *testing.T) {
}
tests := []struct {
name string
fixtures []string
args args
scanExpectation scanner.DriverScanExpectation
want *rpcScanner.ScanResponse
@@ -47,7 +43,6 @@ func TestScanServer_Scan(t *testing.T) {
}{
{
name: "happy path",
fixtures: []string{"testdata/fixtures/vulnerability.yaml", "testdata/fixtures/data-source.yaml"},
args: args{
in: &rpcScanner.ScanRequest{
Target: "alpine:3.11",
@@ -58,6 +53,7 @@ func TestScanServer_Scan(t *testing.T) {
},
scanExpectation: scanner.DriverScanExpectation{
Args: scanner.DriverScanArgs{
CtxAnything: true,
Target: "alpine:3.11",
ImageID: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a",
LayerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"},
@@ -72,10 +68,19 @@ func TestScanServer_Scan(t *testing.T) {
PkgName: "musl",
InstalledVersion: "1.2.3",
FixedVersion: "1.2.4",
SeveritySource: "nvd",
Vulnerability: dbTypes.Vulnerability{
Title: "dos",
Description: "dos vulnerability",
Severity: "MEDIUM",
VendorSeverity: map[dbTypes.SourceID]dbTypes.Severity{
vulnerability.NVD: dbTypes.SeverityMedium,
},
References: []string{"http://example.com"},
LastModifiedDate: utils.MustTimeParse("2020-01-01T01:01:00Z"),
PublishedDate: utils.MustTimeParse("2001-01-01T01:01:00Z"),
},
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2019-0001",
DataSource: &dbTypes.DataSource{
Name: "DOS vulnerabilities",
URL: "https://vuld-db-example.com/",
@@ -147,6 +152,7 @@ func TestScanServer_Scan(t *testing.T) {
},
scanExpectation: scanner.DriverScanExpectation{
Args: scanner.DriverScanArgs{
CtxAnything: true,
Target: "alpine:3.11",
ImageID: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a",
LayerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"},
@@ -161,21 +167,17 @@ func TestScanServer_Scan(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dbtest.InitDB(t, tt.fixtures)
defer db.Close()
mockDriver := new(scanner.MockDriver)
mockDriver.ApplyScanExpectation(tt.scanExpectation)
s := NewScanServer(mockDriver, result.NewClient(db.Config{}))
s := NewScanServer(mockDriver)
got, err := s.Scan(context.Background(), tt.args.in)
if tt.wantErr != "" {
require.NotNil(t, err, tt.name)
assert.Contains(t, err.Error(), tt.wantErr, tt.name)
return
} else {
assert.NoError(t, err, tt.name)
}
assert.NoError(t, err, tt.name)
assert.Equal(t, tt.want, got)
})
}

View File

@@ -1,6 +0,0 @@
- bucket: data-source
pairs:
- key: vulnerability
value:
Name: "DOS vulnerabilities"
URL: "https://vuld-db-example.com/"

View File

@@ -1,13 +0,0 @@
- bucket: vulnerability
pairs:
- key: CVE-2019-0001
value:
Title: dos
Description: dos vulnerability
Severity: MEDIUM
VendorSeverity:
nvd: 2
References:
- http://example.com
LastModifiedDate: "2020-01-01T01:01:00Z"
PublishedDate: "2001-01-01T01:01:00Z"

View File

@@ -11,8 +11,8 @@ import (
"github.com/aquasecurity/fanal/cache"
"github.com/aquasecurity/trivy-db/pkg/db"
"github.com/aquasecurity/trivy/pkg/detector/ospkg"
"github.com/aquasecurity/trivy/pkg/result"
"github.com/aquasecurity/trivy/pkg/scanner/local"
"github.com/aquasecurity/trivy/pkg/vulnerability"
)
// Injectors from inject.go:
@@ -20,9 +20,9 @@ import (
func initializeScanServer(localArtifactCache cache.LocalArtifactCache) *ScanServer {
applierApplier := applier.NewApplier(localArtifactCache)
detector := ospkg.Detector{}
scanner := local.NewScanner(applierApplier, detector)
config := db.Config{}
client := result.NewClient(config)
scanServer := NewScanServer(scanner, client)
client := vulnerability.NewClient(config)
scanner := local.NewScanner(applierApplier, detector, client)
scanServer := NewScanServer(scanner)
return scanServer
}

View File

@@ -1,114 +0,0 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package local
import (
pkgtypes "github.com/aquasecurity/trivy/pkg/types"
mock "github.com/stretchr/testify/mock"
time "time"
types "github.com/aquasecurity/fanal/types"
)
// MockOspkgDetector is an autogenerated mock type for the OspkgDetector type
type MockOspkgDetector struct {
mock.Mock
}
type OspkgDetectorDetectArgs struct {
ImageName string
ImageNameAnything bool
OsFamily string
OsFamilyAnything bool
OsName string
OsNameAnything bool
Repo *types.Repository
RepoAnything bool
Created time.Time
CreatedAnything bool
Pkgs []types.Package
PkgsAnything bool
}
type OspkgDetectorDetectReturns struct {
DetectedVulns []pkgtypes.DetectedVulnerability
Eosl bool
Err error
}
type OspkgDetectorDetectExpectation struct {
Args OspkgDetectorDetectArgs
Returns OspkgDetectorDetectReturns
}
func (_m *MockOspkgDetector) ApplyDetectExpectation(e OspkgDetectorDetectExpectation) {
var args []interface{}
if e.Args.ImageNameAnything {
args = append(args, mock.Anything)
} else {
args = append(args, e.Args.ImageName)
}
if e.Args.OsFamilyAnything {
args = append(args, mock.Anything)
} else {
args = append(args, e.Args.OsFamily)
}
if e.Args.OsNameAnything {
args = append(args, mock.Anything)
} else {
args = append(args, e.Args.OsName)
}
if e.Args.RepoAnything {
args = append(args, mock.Anything)
} else {
args = append(args, e.Args.Repo)
}
if e.Args.CreatedAnything {
args = append(args, mock.Anything)
} else {
args = append(args, e.Args.Created)
}
if e.Args.PkgsAnything {
args = append(args, mock.Anything)
} else {
args = append(args, e.Args.Pkgs)
}
_m.On("Detect", args...).Return(e.Returns.DetectedVulns, e.Returns.Eosl, e.Returns.Err)
}
func (_m *MockOspkgDetector) ApplyDetectExpectations(expectations []OspkgDetectorDetectExpectation) {
for _, e := range expectations {
_m.ApplyDetectExpectation(e)
}
}
// Detect provides a mock function with given fields: imageName, osFamily, osName, created, pkgs
func (_m *MockOspkgDetector) Detect(imageName string, osFamily string, osName string, repo *types.Repository, created time.Time, pkgs []types.Package) ([]pkgtypes.DetectedVulnerability, bool, error) {
ret := _m.Called(imageName, osFamily, osName, repo, created, pkgs)
var r0 []pkgtypes.DetectedVulnerability
if rf, ok := ret.Get(0).(func(string, string, string, *types.Repository, time.Time, []types.Package) []pkgtypes.DetectedVulnerability); ok {
r0 = rf(imageName, osFamily, osName, repo, created, pkgs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]pkgtypes.DetectedVulnerability)
}
}
var r1 bool
if rf, ok := ret.Get(1).(func(string, string, string, *types.Repository, time.Time, []types.Package) bool); ok {
r1 = rf(imageName, osFamily, osName, repo, created, pkgs)
} else {
r1 = ret.Get(1).(bool)
}
var r2 error
if rf, ok := ret.Get(2).(func(string, string, string, *types.Repository, time.Time, []types.Package) error); ok {
r2 = rf(imageName, osFamily, osName, repo, created, pkgs)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}

View File

@@ -1,6 +1,7 @@
package local
import (
"context"
"errors"
"fmt"
"sort"
@@ -12,16 +13,17 @@ import (
"golang.org/x/xerrors"
"github.com/aquasecurity/fanal/analyzer"
_ "github.com/aquasecurity/fanal/analyzer/all"
"github.com/aquasecurity/fanal/applier"
_ "github.com/aquasecurity/fanal/handler/all"
ftypes "github.com/aquasecurity/fanal/types"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy/pkg/detector/library"
ospkgDetector "github.com/aquasecurity/trivy/pkg/detector/ospkg"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/scanner/post"
"github.com/aquasecurity/trivy/pkg/types"
_ "github.com/aquasecurity/fanal/analyzer/all"
_ "github.com/aquasecurity/fanal/handler/all"
"github.com/aquasecurity/trivy/pkg/vulnerability"
)
var (
@@ -35,9 +37,10 @@ var (
// SuperSet binds dependencies for Local scan
var SuperSet = wire.NewSet(
vulnerability.SuperSet,
applier.NewApplier,
wire.Bind(new(Applier), new(applier.Applier)),
ospkgDetector.SuperSet,
wire.Struct(new(ospkgDetector.Detector)),
wire.Bind(new(OspkgDetector), new(ospkgDetector.Detector)),
NewScanner,
)
@@ -56,15 +59,19 @@ type OspkgDetector interface {
type Scanner struct {
applier Applier
ospkgDetector OspkgDetector
vulnClient vulnerability.Client
}
// NewScanner is the factory method for Scanner
func NewScanner(applier Applier, ospkgDetector OspkgDetector) Scanner {
return Scanner{applier: applier, ospkgDetector: ospkgDetector}
func NewScanner(applier Applier, ospkgDetector OspkgDetector, vulnClient vulnerability.Client) Scanner {
return Scanner{
applier: applier,
ospkgDetector: ospkgDetector,
vulnClient: vulnClient}
}
// Scan scans the artifact and return results.
func (s Scanner) Scan(target string, artifactKey string, blobKeys []string, options types.ScanOptions) (types.Results, *ftypes.OS, error) {
func (s Scanner) Scan(ctx context.Context, target, artifactKey string, blobKeys []string, options types.ScanOptions) (types.Results, *ftypes.OS, error) {
artifactDetail, err := s.applier.ApplyLayers(artifactKey, blobKeys)
switch {
case errors.Is(err, analyzer.ErrUnknownOS):
@@ -114,6 +121,25 @@ func (s Scanner) Scan(target string, artifactKey string, blobKeys []string, opti
results = append(results, secretResults...)
}
// For WASM plugins and custom analyzers
if len(artifactDetail.CustomResources) != 0 {
results = append(results, types.Result{
Class: types.ClassCustom,
CustomResources: artifactDetail.CustomResources,
})
}
for i := range results {
// Fill vulnerability details
s.vulnClient.FillInfo(results[i].Vulnerabilities)
}
// Post scanning
results, err = post.Scan(ctx, results)
if err != nil {
return nil, nil, xerrors.Errorf("post scan error: %w", err)
}
return results, artifactDetail.OS, nil
}

View File

@@ -1,9 +1,12 @@
package local
import (
"context"
"errors"
"testing"
"time"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -11,9 +14,11 @@ import (
fos "github.com/aquasecurity/fanal/analyzer/os"
ftypes "github.com/aquasecurity/fanal/types"
"github.com/aquasecurity/trivy-db/pkg/db"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy/pkg/dbtest"
ospkgDetector "github.com/aquasecurity/trivy/pkg/detector/ospkg"
"github.com/aquasecurity/trivy/pkg/detector/ospkg"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/aquasecurity/trivy/pkg/vulnerability"
)
func TestScanner_Scan(t *testing.T) {
@@ -27,7 +32,6 @@ func TestScanner_Scan(t *testing.T) {
args args
fixtures []string
applyLayersExpectation ApplierApplyLayersExpectation
ospkgDetectExpectations []OspkgDetectorDetectExpectation
wantResults types.Results
wantOS *ftypes.OS
wantErr string
@@ -57,6 +61,8 @@ func TestScanner_Scan(t *testing.T) {
{
Name: "musl",
Version: "1.2.3",
SrcName: "musl",
SrcVersion: "1.2.3",
Layer: ftypes.Layer{
DiffID: "sha256:ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
},
@@ -80,40 +86,11 @@ func TestScanner_Scan(t *testing.T) {
},
},
},
ospkgDetectExpectations: []OspkgDetectorDetectExpectation{
{
Args: OspkgDetectorDetectArgs{
OsFamily: "alpine",
OsName: "3.11",
Pkgs: []ftypes.Package{
{
Name: "musl",
Version: "1.2.3",
Layer: ftypes.Layer{
DiffID: "sha256:ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
},
},
},
},
Returns: OspkgDetectorDetectReturns{
DetectedVulns: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2020-9999",
PkgName: "musl",
InstalledVersion: "1.2.3",
FixedVersion: "1.2.4",
Layer: ftypes.Layer{
DiffID: "sha256:ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
},
},
},
Eosl: true,
},
},
},
wantResults: types.Results{
{
Target: "alpine:latest (alpine 3.11)",
Class: types.ClassOSPkg,
Type: fos.Alpine,
Vulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2020-9999",
@@ -123,13 +100,19 @@ func TestScanner_Scan(t *testing.T) {
Layer: ftypes.Layer{
DiffID: "sha256:ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
},
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-9999",
Vulnerability: dbTypes.Vulnerability{
Title: "dos",
Description: "dos vulnerability",
Severity: "HIGH",
},
},
},
Class: types.ClassOSPkg,
Type: fos.Alpine,
},
{
Target: "/app/Gemfile.lock",
Class: types.ClassLangPkg,
Type: ftypes.Bundler,
Vulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2014-0081",
@@ -139,10 +122,19 @@ func TestScanner_Scan(t *testing.T) {
Layer: ftypes.Layer{
DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33",
},
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2014-0081",
Vulnerability: dbTypes.Vulnerability{
Title: "xss",
Description: "xss vulnerability",
Severity: "MEDIUM",
References: []string{
"http://example.com",
},
LastModifiedDate: lo.ToPtr(time.Date(2020, 2, 1, 1, 1, 0, 0, time.UTC)),
PublishedDate: lo.ToPtr(time.Date(2020, 1, 1, 1, 1, 0, 0, time.UTC)),
},
},
},
Class: types.ClassLangPkg,
Type: ftypes.Bundler,
},
},
wantOS: &ftypes.OS{
@@ -177,6 +169,8 @@ func TestScanner_Scan(t *testing.T) {
{
Name: "musl",
Version: "1.2.3",
SrcName: "musl",
SrcVersion: "1.2.3",
Layer: ftypes.Layer{
DiffID: "sha256:ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
},
@@ -184,6 +178,8 @@ func TestScanner_Scan(t *testing.T) {
{
Name: "ausl",
Version: "1.2.3",
SrcName: "ausl",
SrcVersion: "1.2.3",
Layer: ftypes.Layer{
DiffID: "sha256:bbf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
},
@@ -207,51 +203,17 @@ func TestScanner_Scan(t *testing.T) {
},
},
},
ospkgDetectExpectations: []OspkgDetectorDetectExpectation{
{
Args: OspkgDetectorDetectArgs{
OsFamily: "alpine",
OsName: "3.11",
Pkgs: []ftypes.Package{
{
Name: "musl",
Version: "1.2.3",
Layer: ftypes.Layer{
DiffID: "sha256:ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
},
},
{
Name: "ausl",
Version: "1.2.3",
Layer: ftypes.Layer{
DiffID: "sha256:bbf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
},
},
},
},
Returns: OspkgDetectorDetectReturns{
DetectedVulns: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2020-9999",
PkgName: "musl",
InstalledVersion: "1.2.3",
FixedVersion: "1.2.4",
Layer: ftypes.Layer{
DiffID: "sha256:ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
},
},
},
Eosl: false,
},
},
},
wantResults: types.Results{
{
Target: "alpine:latest (alpine 3.11)",
Class: types.ClassOSPkg,
Type: fos.Alpine,
Packages: []ftypes.Package{
{
Name: "ausl",
Version: "1.2.3",
SrcName: "ausl",
SrcVersion: "1.2.3",
Layer: ftypes.Layer{
DiffID: "sha256:bbf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
},
@@ -259,6 +221,8 @@ func TestScanner_Scan(t *testing.T) {
{
Name: "musl",
Version: "1.2.3",
SrcName: "musl",
SrcVersion: "1.2.3",
Layer: ftypes.Layer{
DiffID: "sha256:ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
},
@@ -273,13 +237,19 @@ func TestScanner_Scan(t *testing.T) {
Layer: ftypes.Layer{
DiffID: "sha256:ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
},
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-9999",
Vulnerability: dbTypes.Vulnerability{
Title: "dos",
Description: "dos vulnerability",
Severity: "HIGH",
},
},
},
Class: types.ClassOSPkg,
Type: fos.Alpine,
},
{
Target: "/app/Gemfile.lock",
Class: types.ClassLangPkg,
Type: ftypes.Bundler,
Packages: []ftypes.Package{
{
Name: "rails",
@@ -298,15 +268,25 @@ func TestScanner_Scan(t *testing.T) {
Layer: ftypes.Layer{
DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33",
},
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2014-0081",
Vulnerability: dbTypes.Vulnerability{
Title: "xss",
Description: "xss vulnerability",
Severity: "MEDIUM",
References: []string{
"http://example.com",
},
LastModifiedDate: lo.ToPtr(time.Date(2020, 2, 1, 1, 1, 0, 0, time.UTC)),
PublishedDate: lo.ToPtr(time.Date(2020, 1, 1, 1, 1, 0, 0, time.UTC)),
},
},
},
Class: types.ClassLangPkg,
Type: ftypes.Bundler,
},
},
wantOS: &ftypes.OS{
Family: "alpine",
Name: "3.11",
Eosl: true,
},
},
{
@@ -357,6 +337,17 @@ func TestScanner_Scan(t *testing.T) {
Layer: ftypes.Layer{
DiffID: "sha256:9922bc15eeefe1637b803ef2106f178152ce19a391f24aec838cbe2e48e73303",
},
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2014-0081",
Vulnerability: dbTypes.Vulnerability{
Title: "xss",
Description: "xss vulnerability",
Severity: "MEDIUM",
References: []string{
"http://example.com",
},
LastModifiedDate: lo.ToPtr(time.Date(2020, 2, 1, 1, 1, 0, 0, time.UTC)),
PublishedDate: lo.ToPtr(time.Date(2020, 1, 1, 1, 1, 0, 0, time.UTC)),
},
},
},
Class: types.ClassLangPkg,
@@ -405,17 +396,6 @@ func TestScanner_Scan(t *testing.T) {
Err: analyzer.ErrNoPkgsDetected,
},
},
ospkgDetectExpectations: []OspkgDetectorDetectExpectation{
{
Args: OspkgDetectorDetectArgs{
OsFamily: "alpine",
OsName: "3.11",
},
Returns: OspkgDetectorDetectReturns{
Eosl: false,
},
},
},
wantResults: types.Results{
{
Target: "alpine:latest (alpine 3.11)",
@@ -433,6 +413,17 @@ func TestScanner_Scan(t *testing.T) {
Layer: ftypes.Layer{
DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33",
},
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2014-0081",
Vulnerability: dbTypes.Vulnerability{
Title: "xss",
Description: "xss vulnerability",
Severity: "MEDIUM",
References: []string{
"http://example.com",
},
LastModifiedDate: lo.ToPtr(time.Date(2020, 2, 1, 1, 1, 0, 0, time.UTC)),
PublishedDate: lo.ToPtr(time.Date(2020, 1, 1, 1, 1, 0, 0, time.UTC)),
},
},
},
Class: types.ClassLangPkg,
@@ -442,6 +433,7 @@ func TestScanner_Scan(t *testing.T) {
wantOS: &ftypes.OS{
Family: "alpine",
Name: "3.11",
Eosl: true,
},
},
{
@@ -483,17 +475,6 @@ func TestScanner_Scan(t *testing.T) {
},
},
},
ospkgDetectExpectations: []OspkgDetectorDetectExpectation{
{
Args: OspkgDetectorDetectArgs{
OsFamily: "fedora",
OsName: "27",
},
Returns: OspkgDetectorDetectReturns{
Err: ospkgDetector.ErrUnsupportedOS,
},
},
},
wantResults: types.Results{
{
Target: "/app/Gemfile.lock",
@@ -506,6 +487,17 @@ func TestScanner_Scan(t *testing.T) {
Layer: ftypes.Layer{
DiffID: "sha256:9922bc15eeefe1637b803ef2106f178152ce19a391f24aec838cbe2e48e73303",
},
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2014-0081",
Vulnerability: dbTypes.Vulnerability{
Title: "xss",
Description: "xss vulnerability",
Severity: "MEDIUM",
References: []string{
"http://example.com",
},
LastModifiedDate: lo.ToPtr(time.Date(2020, 2, 1, 1, 1, 0, 0, time.UTC)),
PublishedDate: lo.ToPtr(time.Date(2020, 1, 1, 1, 1, 0, 0, time.UTC)),
},
},
},
Class: types.ClassLangPkg,
@@ -564,7 +556,12 @@ func TestScanner_Scan(t *testing.T) {
Name: "3.11",
},
Packages: []ftypes.Package{
{Name: "musl", Version: "1.2.3"},
{
Name: "musl",
Version: "1.2.3",
SrcName: "musl",
SrcVersion: "1.2.3",
},
},
Applications: []ftypes.Application{
{
@@ -609,6 +606,17 @@ func TestScanner_Scan(t *testing.T) {
Layer: ftypes.Layer{
DiffID: "sha256:5cb2a5009179b1e78ecfef81a19756328bb266456cf9a9dbbcf9af8b83b735f0",
},
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2014-0081",
Vulnerability: dbTypes.Vulnerability{
Title: "xss",
Description: "xss vulnerability",
Severity: "MEDIUM",
References: []string{
"http://example.com",
},
LastModifiedDate: lo.ToPtr(time.Date(2020, 2, 1, 1, 1, 0, 0, time.UTC)),
PublishedDate: lo.ToPtr(time.Date(2020, 1, 1, 1, 1, 0, 0, time.UTC)),
},
},
},
Class: types.ClassLangPkg,
@@ -806,61 +814,6 @@ func TestScanner_Scan(t *testing.T) {
},
wantErr: "failed to apply layers",
},
{
name: "sad path: ospkgDetector.Detect returns an error",
args: args{
target: "alpine:latest",
layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"},
options: types.ScanOptions{
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
SecurityChecks: []string{types.SecurityCheckVulnerability},
},
},
fixtures: []string{"testdata/fixtures/happy.yaml"},
applyLayersExpectation: ApplierApplyLayersExpectation{
Args: ApplierApplyLayersArgs{
BlobIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"},
},
Returns: ApplierApplyLayersReturns{
Detail: ftypes.ArtifactDetail{
OS: &ftypes.OS{
Family: "alpine",
Name: "3.11",
},
Packages: []ftypes.Package{
{
Name: "musl",
Version: "1.2.3",
Layer: ftypes.Layer{
DiffID: "sha256:ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
},
},
},
},
},
},
ospkgDetectExpectations: []OspkgDetectorDetectExpectation{
{
Args: OspkgDetectorDetectArgs{
OsFamily: "alpine",
OsName: "3.11",
Pkgs: []ftypes.Package{
{
Name: "musl",
Version: "1.2.3",
Layer: ftypes.Layer{
DiffID: "sha256:ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
},
},
},
},
Returns: OspkgDetectorDetectReturns{
Err: errors.New("error"),
},
},
},
wantErr: "failed to scan OS packages",
},
{
name: "sad path: library.Detect returns an error",
args: args{
@@ -921,11 +874,8 @@ func TestScanner_Scan(t *testing.T) {
applier := new(MockApplier)
applier.ApplyApplyLayersExpectation(tt.applyLayersExpectation)
ospkgDetector := new(MockOspkgDetector)
ospkgDetector.ApplyDetectExpectations(tt.ospkgDetectExpectations)
s := NewScanner(applier, ospkgDetector)
gotResults, gotOS, err := s.Scan(tt.args.target, "", tt.args.layerIDs, tt.args.options)
s := NewScanner(applier, ospkg.Detector{}, vulnerability.NewClient(db.Config{}))
gotResults, gotOS, err := s.Scan(context.Background(), tt.args.target, "", tt.args.layerIDs, tt.args.options)
if tt.wantErr != "" {
require.NotNil(t, err, tt.name)
require.Contains(t, err.Error(), tt.wantErr, tt.name)
@@ -937,7 +887,6 @@ func TestScanner_Scan(t *testing.T) {
assert.Equal(t, tt.wantOS, gotOS)
applier.AssertExpectations(t)
ospkgDetector.AssertExpectations(t)
})
}
}

View File

@@ -1,3 +1,10 @@
- bucket: alpine 3.11
pairs:
- bucket: musl
pairs:
- key: CVE-2020-9999
value:
FixedVersion: 1.2.4
- bucket: "rubygems::GitHub Security Advisory RubyGems"
pairs:
- bucket: rails
@@ -25,4 +32,19 @@
- ">= 8.0.0, < 8.22.1"
- ">= 7.0.0, < 7.30.3"
- "< 6.20.12"
- bucket: vulnerability
pairs:
- key: CVE-2020-9999
value:
Title: dos
Description: dos vulnerability
Severity: HIGH
- key: CVE-2014-0081
value:
Title: xss
Description: xss vulnerability
Severity: MEDIUM
References:
- http://example.com
LastModifiedDate: "2020-02-01T01:01:00Z"
PublishedDate: "2020-01-01T01:01:00Z"

View File

@@ -3,6 +3,8 @@
package scanner
import (
"context"
fanaltypes "github.com/aquasecurity/fanal/types"
mock "github.com/stretchr/testify/mock"
@@ -15,6 +17,8 @@ type MockDriver struct {
}
type DriverScanArgs struct {
Ctx context.Context
CtxAnything bool
Target string
TargetAnything bool
ImageID string
@@ -38,6 +42,11 @@ type DriverScanExpectation struct {
func (_m *MockDriver) ApplyScanExpectation(e DriverScanExpectation) {
var args []interface{}
if e.Args.CtxAnything {
args = append(args, mock.Anything)
} else {
args = append(args, e.Args.Ctx)
}
if e.Args.TargetAnything {
args = append(args, mock.Anything)
} else {
@@ -67,13 +76,13 @@ func (_m *MockDriver) ApplyScanExpectations(expectations []DriverScanExpectation
}
}
// Scan provides a mock function with given fields: target, imageID, layerIDs, options
func (_m *MockDriver) Scan(target string, artifactKey string, blobKeys []string, options types.ScanOptions) (types.Results, *fanaltypes.OS, error) {
ret := _m.Called(target, artifactKey, blobKeys, options)
// Scan provides a mock function with given fields: ctx, target, imageID, layerIDs, options
func (_m *MockDriver) Scan(ctx context.Context, target string, artifactKey string, blobKeys []string, options types.ScanOptions) (types.Results, *fanaltypes.OS, error) {
ret := _m.Called(ctx, target, artifactKey, blobKeys, options)
var r0 types.Results
if rf, ok := ret.Get(0).(func(string, string, []string, types.ScanOptions) types.Results); ok {
r0 = rf(target, artifactKey, blobKeys, options)
if rf, ok := ret.Get(0).(func(context.Context, string, string, []string, types.ScanOptions) types.Results); ok {
r0 = rf(ctx, target, artifactKey, blobKeys, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(types.Results)
@@ -81,8 +90,8 @@ func (_m *MockDriver) Scan(target string, artifactKey string, blobKeys []string,
}
var r1 *fanaltypes.OS
if rf, ok := ret.Get(1).(func(string, string, []string, types.ScanOptions) *fanaltypes.OS); ok {
r1 = rf(target, artifactKey, blobKeys, options)
if rf, ok := ret.Get(1).(func(context.Context, string, string, []string, types.ScanOptions) *fanaltypes.OS); ok {
r1 = rf(ctx, target, artifactKey, blobKeys, options)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*fanaltypes.OS)
@@ -90,8 +99,8 @@ func (_m *MockDriver) Scan(target string, artifactKey string, blobKeys []string,
}
var r2 error
if rf, ok := ret.Get(2).(func(string, string, []string, types.ScanOptions) error); ok {
r2 = rf(target, artifactKey, blobKeys, options)
if rf, ok := ret.Get(2).(func(context.Context, string, string, []string, types.ScanOptions) error); ok {
r2 = rf(ctx, target, artifactKey, blobKeys, options)
} else {
r2 = ret.Error(2)
}

View File

@@ -0,0 +1,45 @@
package post
import (
"context"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/types"
)
type Scanner interface {
Name() string
Version() int
PostScan(ctx context.Context, results types.Results) (types.Results, error)
}
func RegisterPostScanner(s Scanner) {
// Avoid duplication
postScanners[s.Name()] = s
}
func DeregisterPostScanner(name string) {
delete(postScanners, name)
}
func ScannerVersions() map[string]int {
versions := map[string]int{}
for _, s := range postScanners {
versions[s.Name()] = s.Version()
}
return versions
}
var postScanners = map[string]Scanner{}
func Scan(ctx context.Context, results types.Results) (types.Results, error) {
var err error
for _, s := range postScanners {
results, err = s.PostScan(ctx, results)
if err != nil {
return nil, xerrors.Errorf("%s post scan error: %w", s.Name(), err)
}
}
return results, nil
}

View File

@@ -0,0 +1,105 @@
package post_test
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy/pkg/scanner/post"
"github.com/stretchr/testify/require"
"github.com/aquasecurity/trivy/pkg/types"
)
type testPostScanner struct{}
func (testPostScanner) Name() string {
return "test"
}
func (testPostScanner) Version() int {
return 1
}
func (testPostScanner) PostScan(ctx context.Context, results types.Results) (types.Results, error) {
for i, r := range results {
if r.Target == "bad" {
return nil, errors.New("bad")
}
for j := range r.Vulnerabilities {
results[i].Vulnerabilities[j].Severity = "LOW"
}
}
return results, nil
}
func TestScan(t *testing.T) {
tests := []struct {
name string
results types.Results
want types.Results
wantErr bool
}{
{
name: "happy path",
results: types.Results{
{
Target: "test",
Vulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2022-0001",
PkgName: "musl",
InstalledVersion: "1.2.3",
FixedVersion: "1.2.4",
Vulnerability: dbTypes.Vulnerability{
Severity: "CRITICAL",
},
},
},
},
},
want: types.Results{
{
Target: "test",
Vulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2022-0001",
PkgName: "musl",
InstalledVersion: "1.2.3",
FixedVersion: "1.2.4",
Vulnerability: dbTypes.Vulnerability{
Severity: "LOW",
},
},
},
},
},
},
{
name: "sad path",
results: types.Results{
{
Target: "bad",
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := testPostScanner{}
post.RegisterPostScanner(s)
defer func() {
post.DeregisterPostScanner(s.Name())
}()
results, err := post.Scan(context.Background(), tt.results)
require.Equal(t, err != nil, tt.wantErr)
assert.Equal(t, results, tt.want)
})
}
}

View File

@@ -96,7 +96,7 @@ type Scanner struct {
// Driver defines operations of scanner
type Driver interface {
Scan(target string, artifactKey string, blobKeys []string, options types.ScanOptions) (
Scan(ctx context.Context, target, artifactKey string, blobKeys []string, options types.ScanOptions) (
results types.Results, osFound *ftypes.OS, err error)
}
@@ -117,7 +117,7 @@ func (s Scanner) ScanArtifact(ctx context.Context, options types.ScanOptions) (t
}
}()
results, osFound, err := s.driver.Scan(artifactInfo.Name, artifactInfo.ID, artifactInfo.BlobIDs, options)
results, osFound, err := s.driver.Scan(ctx, artifactInfo.Name, artifactInfo.ID, artifactInfo.BlobIDs, options)
if err != nil {
return types.Report{}, xerrors.Errorf("scan failed: %w", err)
}

View File

@@ -51,6 +51,7 @@ func TestScanner_ScanArtifact(t *testing.T) {
},
scanExpectation: DriverScanExpectation{
Args: DriverScanArgs{
CtxAnything: true,
Target: "alpine:3.11",
ImageID: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a",
LayerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"},
@@ -173,6 +174,7 @@ func TestScanner_ScanArtifact(t *testing.T) {
},
scanExpectation: DriverScanExpectation{
Args: DriverScanArgs{
CtxAnything: true,
Target: "alpine:3.11",
ImageID: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a",
LayerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"},

View File

@@ -40,6 +40,7 @@ const (
ClassLangPkg = "lang-pkgs"
ClassConfig = "config"
ClassSecret = "secret"
ClassCustom = "custom"
)
// Result holds a target and detected vulnerabilities

View File

@@ -1,9 +1,5 @@
package types
import (
"golang.org/x/exp/slices"
)
// VulnType represents vulnerability type
type VulnType = string
@@ -34,22 +30,6 @@ const (
)
var (
vulnTypes = []string{VulnTypeOS, VulnTypeLibrary}
securityChecks = []string{SecurityCheckVulnerability, SecurityCheckConfig, SecurityCheckSecret}
VulnTypes = []string{VulnTypeOS, VulnTypeLibrary}
SecurityChecks = []string{SecurityCheckVulnerability, SecurityCheckConfig, SecurityCheckSecret}
)
// NewVulnType returns an instance of VulnType
func NewVulnType(s string) VulnType {
if slices.Contains(vulnTypes, s) {
return s
}
return VulnTypeUnknown
}
// NewSecurityCheck returns an instance of SecurityCheck
func NewSecurityCheck(s string) SecurityCheck {
if slices.Contains(securityChecks, s) {
return s
}
return SecurityCheckUnknown
}

View File

@@ -11,6 +11,10 @@ import (
"golang.org/x/xerrors"
)
const (
xdgDataHome = "XDG_DATA_HOME"
)
var cacheDir string
// DefaultCacheDir returns/creates the cache-dir to be used for trivy operations
@@ -32,6 +36,16 @@ func SetCacheDir(dir string) {
cacheDir = dir
}
func HomeDir() string {
dataHome := os.Getenv(xdgDataHome)
if dataHome != "" {
return dataHome
}
homeDir, _ := os.UserHomeDir()
return homeDir
}
// CopyFile copies the file content from scr to dst
func CopyFile(src, dst string) (int64, error) {
sourceFileStat, err := os.Stat(src)

View File

@@ -1,6 +0,0 @@
# vulnerabilities
CVE-2019-0001
CVE-2019-0002
# misconfigurations
ID100

View File

@@ -0,0 +1,6 @@
- bucket: vulnerability
pairs:
- key: CVE-2019-0001
value:
Title:
- aaa

View File

@@ -0,0 +1,57 @@
- bucket: vulnerability
pairs:
- key: CVE-2019-0001
value:
Title: dos
Description: dos vulnerability
Severity: MEDIUM
References:
- http://example.com
LastModifiedDate: "2020-01-01T01:01:00Z"
PublishedDate: "2001-01-01T01:01:00Z"
- key: CVE-2019-0004
value:
Title: dos
Description: dos vulnerability
Severity: MEDIUM
VendorSeverity:
redhat: 1
CVSS:
nvd:
V2Vector: AV:N/AC:L/Au:N/C:P/I:P/A:P
V2Score: 4.5
V3Vector: CVSS:3.0/PR:N/UI:N/S:U/C:H/I:H/A:H
V3Score: 5.6
redhat:
V2Vector: AV:N/AC:M/Au:N/C:N/I:P/A:N
V2Score: 7.8
V3Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
V3Score: 9.8
References:
- http://example.com
CweIDs:
- CWE-311
- key: CVE-2019-0005
value:
Title: COVID-19
Description: a nasty virus vulnerability for humans
Severity: MEDIUM
VendorSeverity:
ghsa: 4
References:
- "https://www.who.int/emergencies/diseases/novel-coronavirus-2019"
- key: RUSTSEC-2018-0017
value:
Title: dos
Description: dos vulnerability
Severity: UNKNOWN
VendorSeverity:
nvd: 1
LastModifiedDate: "2020-01-01T01:01:00Z"
PublishedDate: "2001-01-01T01:01:00Z"
- key: GHSA-28fw-88hq-6jmm
value:
Title: dos
Description: dos vulnerability
References:
- http://example.com

View File

@@ -0,0 +1,117 @@
package vulnerability
import (
"strings"
"github.com/google/wire"
"github.com/aquasecurity/trivy-db/pkg/db"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/types"
)
var (
primaryURLPrefixes = map[dbTypes.SourceID][]string{
vulnerability.Debian: {"http://www.debian.org", "https://www.debian.org"},
vulnerability.Ubuntu: {"http://www.ubuntu.com", "https://usn.ubuntu.com"},
vulnerability.RedHat: {"https://access.redhat.com"},
vulnerability.SuseCVRF: {"http://lists.opensuse.org", "https://lists.opensuse.org"},
vulnerability.OracleOVAL: {"http://linux.oracle.com/errata", "https://linux.oracle.com/errata"},
vulnerability.NodejsSecurityWg: {"https://www.npmjs.com", "https://hackerone.com"},
vulnerability.RubySec: {"https://groups.google.com"},
}
)
// SuperSet binds the dependencies
var SuperSet = wire.NewSet(
wire.Struct(new(db.Config)),
NewClient,
)
// Client manipulates vulnerabilities
type Client struct {
dbc db.Operation
}
// NewClient is the factory method for Client
func NewClient(dbc db.Config) Client {
return Client{dbc: dbc}
}
// FillInfo fills extra info in vulnerability objects
func (c Client) FillInfo(vulns []types.DetectedVulnerability) {
for i := range vulns {
vulnID := vulns[i].VulnerabilityID
vuln, err := c.dbc.GetVulnerability(vulnID)
if err != nil {
log.Logger.Warnf("Error while getting vulnerability details: %s", err)
continue
}
// Detect the data source
var source dbTypes.SourceID
if vulns[i].DataSource != nil {
source = vulns[i].DataSource.ID
}
// Select the severity according to the detected source.
severity, severitySource := c.getVendorSeverity(&vuln, source)
// The vendor might provide package-specific severity like Debian.
// For example, CVE-2015-2328 in Debian has "unimportant" for mongodb and "low" for pcre3.
// In that case, we keep the severity as is.
if vulns[i].SeveritySource != "" {
severity = vulns[i].Severity
severitySource = vulns[i].SeveritySource
}
// Add the vulnerability detail
vulns[i].Vulnerability = vuln
vulns[i].Severity = severity
vulns[i].SeveritySource = severitySource
vulns[i].PrimaryURL = c.getPrimaryURL(vulnID, vuln.References, source)
}
}
func (c Client) getVendorSeverity(vuln *dbTypes.Vulnerability, source dbTypes.SourceID) (string, dbTypes.SourceID) {
if vs, ok := vuln.VendorSeverity[source]; ok {
return vs.String(), source
}
// Try NVD as a fallback if it exists
if vs, ok := vuln.VendorSeverity[vulnerability.NVD]; ok {
return vs.String(), vulnerability.NVD
}
if vuln.Severity == "" {
return dbTypes.SeverityUnknown.String(), ""
}
return vuln.Severity, ""
}
func (c Client) getPrimaryURL(vulnID string, refs []string, source dbTypes.SourceID) string {
switch {
case strings.HasPrefix(vulnID, "CVE-"):
return "https://avd.aquasec.com/nvd/" + strings.ToLower(vulnID)
case strings.HasPrefix(vulnID, "RUSTSEC-"):
return "https://osv.dev/vulnerability/" + vulnID
case strings.HasPrefix(vulnID, "GHSA-"):
return "https://github.com/advisories/" + vulnID
case strings.HasPrefix(vulnID, "TEMP-"):
return "https://security-tracker.debian.org/tracker/" + vulnID
}
prefixes := primaryURLPrefixes[source]
for _, pre := range prefixes {
for _, ref := range refs {
if strings.HasPrefix(ref, pre) {
return ref
}
}
}
return ""
}

View File

@@ -0,0 +1,246 @@
package vulnerability_test
import (
"testing"
"github.com/aquasecurity/trivy-db/pkg/db"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy-db/pkg/utils"
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
"github.com/aquasecurity/trivy/pkg/dbtest"
"github.com/aquasecurity/trivy/pkg/types"
vuln "github.com/aquasecurity/trivy/pkg/vulnerability"
"github.com/stretchr/testify/assert"
)
func TestClient_FillInfo(t *testing.T) {
tests := []struct {
name string
fixtures []string
vulns []types.DetectedVulnerability
expectedVulnerabilities []types.DetectedVulnerability
}{
{
name: "happy path, with only OS vulnerability but no vendor severity, no NVD",
fixtures: []string{"testdata/fixtures/vulnerability.yaml"},
vulns: []types.DetectedVulnerability{
{VulnerabilityID: "CVE-2019-0001"},
},
expectedVulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0001",
Vulnerability: dbTypes.Vulnerability{
Title: "dos",
Description: "dos vulnerability",
Severity: dbTypes.SeverityMedium.String(),
References: []string{"http://example.com"},
LastModifiedDate: utils.MustTimeParse("2020-01-01T01:01:00Z"),
PublishedDate: utils.MustTimeParse("2001-01-01T01:01:00Z"),
},
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2019-0001",
},
},
},
{
name: "happy path, with only OS vulnerability but no vendor severity, yes NVD",
fixtures: []string{"testdata/fixtures/vulnerability.yaml"},
vulns: []types.DetectedVulnerability{
{
VulnerabilityID: "RUSTSEC-2018-0017",
DataSource: &dbTypes.DataSource{
ID: vulnerability.OSV,
Name: "RustSec Advisory Database",
URL: "https://github.com/RustSec/advisory-db",
},
},
},
expectedVulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "RUSTSEC-2018-0017",
Vulnerability: dbTypes.Vulnerability{
Title: "dos",
Description: "dos vulnerability",
Severity: dbTypes.SeverityLow.String(),
VendorSeverity: dbTypes.VendorSeverity{
vulnerability.NVD: dbTypes.SeverityLow,
},
LastModifiedDate: utils.MustTimeParse("2020-01-01T01:01:00Z"),
PublishedDate: utils.MustTimeParse("2001-01-01T01:01:00Z"),
},
SeveritySource: vulnerability.NVD,
PrimaryURL: "https://osv.dev/vulnerability/RUSTSEC-2018-0017",
DataSource: &dbTypes.DataSource{
ID: vulnerability.OSV,
Name: "RustSec Advisory Database",
URL: "https://github.com/RustSec/advisory-db",
},
},
},
},
{
name: "happy path, with only OS vulnerability but no severity, no vendor severity, no NVD",
fixtures: []string{"testdata/fixtures/vulnerability.yaml"},
vulns: []types.DetectedVulnerability{
{
VulnerabilityID: "GHSA-28fw-88hq-6jmm",
DataSource: &dbTypes.DataSource{
ID: vulnerability.GHSA,
Name: "GitHub Security Advisory Pip",
URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip",
},
},
},
expectedVulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "GHSA-28fw-88hq-6jmm",
Vulnerability: dbTypes.Vulnerability{
Title: "dos",
Description: "dos vulnerability",
Severity: dbTypes.SeverityUnknown.String(),
References: []string{"http://example.com"},
},
PrimaryURL: "https://github.com/advisories/GHSA-28fw-88hq-6jmm",
DataSource: &dbTypes.DataSource{
ID: vulnerability.GHSA,
Name: "GitHub Security Advisory Pip",
URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip",
},
},
},
},
{
name: "happy path, with only OS vulnerability, yes vendor severity, with both NVD and CVSS info",
fixtures: []string{"testdata/fixtures/vulnerability.yaml"},
vulns: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0004",
DataSource: &dbTypes.DataSource{
ID: vulnerability.RedHat,
Name: "Red Hat OVAL v2",
URL: "https://www.redhat.com/security/data/oval/v2/",
},
},
},
expectedVulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0004",
Vulnerability: dbTypes.Vulnerability{
Title: "dos",
Description: "dos vulnerability",
Severity: dbTypes.SeverityLow.String(),
VendorSeverity: dbTypes.VendorSeverity{
vulnerability.RedHat: dbTypes.SeverityLow,
},
CweIDs: []string{"CWE-311"},
References: []string{"http://example.com"},
CVSS: map[dbTypes.SourceID]dbTypes.CVSS{
vulnerability.NVD: {
V2Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P",
V2Score: 4.5,
V3Vector: "CVSS:3.0/PR:N/UI:N/S:U/C:H/I:H/A:H",
V3Score: 5.6,
},
vulnerability.RedHat: {
V2Vector: "AV:N/AC:M/Au:N/C:N/I:P/A:N",
V2Score: 7.8,
V3Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
V3Score: 9.8,
},
},
},
SeveritySource: vulnerability.RedHat,
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2019-0004",
DataSource: &dbTypes.DataSource{
ID: vulnerability.RedHat,
Name: "Red Hat OVAL v2",
URL: "https://www.redhat.com/security/data/oval/v2/",
},
},
},
},
{
name: "happy path, with only library vulnerability",
fixtures: []string{"testdata/fixtures/vulnerability.yaml"},
vulns: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0005",
DataSource: &dbTypes.DataSource{
ID: vulnerability.GHSA,
Name: "GitHub Security Advisory Pip",
URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip",
},
},
},
expectedVulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0005",
Vulnerability: dbTypes.Vulnerability{
Title: "COVID-19",
Description: "a nasty virus vulnerability for humans",
Severity: dbTypes.SeverityCritical.String(),
VendorSeverity: dbTypes.VendorSeverity{
vulnerability.GHSA: dbTypes.SeverityCritical,
},
References: []string{"https://www.who.int/emergencies/diseases/novel-coronavirus-2019"},
},
SeveritySource: vulnerability.GHSA,
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2019-0005",
DataSource: &dbTypes.DataSource{
ID: vulnerability.GHSA,
Name: "GitHub Security Advisory Pip",
URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip",
},
},
},
},
{
name: "happy path, with package-specific severity",
fixtures: []string{"testdata/fixtures/vulnerability.yaml"},
vulns: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0001",
SeveritySource: vulnerability.Debian,
Vulnerability: dbTypes.Vulnerability{
Severity: dbTypes.SeverityLow.String(),
},
},
},
expectedVulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-0001",
SeveritySource: vulnerability.Debian,
Vulnerability: dbTypes.Vulnerability{
Title: "dos",
Description: "dos vulnerability",
Severity: dbTypes.SeverityLow.String(),
References: []string{"http://example.com"},
LastModifiedDate: utils.MustTimeParse("2020-01-01T01:01:00Z"),
PublishedDate: utils.MustTimeParse("2001-01-01T01:01:00Z"),
},
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2019-0001",
},
},
},
{
name: "GetVulnerability returns an error",
fixtures: []string{"testdata/fixtures/sad.yaml"},
vulns: []types.DetectedVulnerability{
{VulnerabilityID: "CVE-2019-0004"},
},
expectedVulnerabilities: []types.DetectedVulnerability{
{VulnerabilityID: "CVE-2019-0004"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dbtest.InitDB(t, tt.fixtures)
defer db.Close()
c := vuln.NewClient(db.Config{})
c.FillInfo(tt.vulns)
assert.Equal(t, tt.expectedVulnerabilities, tt.vulns, tt.name)
})
}
}