mirror of
https://github.com/project-zot/zot.git
synced 2026-06-15 11:37:56 +08:00
feat: add trivy-based sbom artifact generation support (#4088)
fixes issue #4067 Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>
This commit is contained in:
committed by
GitHub
parent
d8fb19819b
commit
4e4d00a0a6
@@ -33,3 +33,5 @@ vendor/
|
|||||||
examples/config-sync-localhost.json
|
examples/config-sync-localhost.json
|
||||||
node_modules
|
node_modules
|
||||||
zot-test
|
zot-test
|
||||||
|
|
||||||
|
.gotmp/
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ ZUI_BUILD_PATH := ""
|
|||||||
ZUI_VERSION := commit-34deb3d
|
ZUI_VERSION := commit-34deb3d
|
||||||
ZUI_REPO_OWNER := project-zot
|
ZUI_REPO_OWNER := project-zot
|
||||||
ZUI_REPO_NAME := zui
|
ZUI_REPO_NAME := zui
|
||||||
SWAGGER_VERSION := v1.16.2
|
SWAGGER_VERSION := v1.16.6
|
||||||
STACKER := $(TOOLSDIR)/bin/stacker
|
STACKER := $(TOOLSDIR)/bin/stacker
|
||||||
STACKER_VERSION := v1.1.0-rc3
|
STACKER_VERSION := v1.1.0-rc3
|
||||||
KIND := $(TOOLSDIR)/bin/kind
|
KIND := $(TOOLSDIR)/bin/kind
|
||||||
@@ -364,7 +364,7 @@ check: ./.golangci.yaml $(GOLINTER)
|
|||||||
.PHONY: swagger
|
.PHONY: swagger
|
||||||
swagger:
|
swagger:
|
||||||
swag -v || go install github.com/swaggo/swag/cmd/swag@$(SWAGGER_VERSION)
|
swag -v || go install github.com/swaggo/swag/cmd/swag@$(SWAGGER_VERSION)
|
||||||
swag init --parseDependency -o swagger -g pkg/api/routes.go -q
|
swag init --parseDependency --exclude pkg/extensions/search/cve/trivy -o swagger -g pkg/api/routes.go -q
|
||||||
|
|
||||||
.PHONY: update-licenses
|
.PHONY: update-licenses
|
||||||
# note: for predictable output of below sort command we use locale LC_ALL=C
|
# note: for predictable output of below sort command we use locale LC_ALL=C
|
||||||
|
|||||||
+3
-1
@@ -1296,6 +1296,8 @@ A minimal configuration only sets how often the DB is refreshed; zot applies def
|
|||||||
|
|
||||||
To set those options explicitly (for example to mirror standalone Trivy’s `--vuln-severity-source` behavior), use a `trivy` object under `cve`:
|
To set those options explicitly (for example to mirror standalone Trivy’s `--vuln-severity-source` behavior), use a `trivy` object under `cve`:
|
||||||
|
|
||||||
- [config-cve-trivy.json](config-cve-trivy.json) — shows optional `dbRepository`, `javaDBRepository`, and `vulnSeveritySources`.
|
- [config-cve-trivy.json](config-cve-trivy.json) — shows optional `dbRepository`, `javaDBRepository`, `vulnSeveritySources`, and `sbom`.
|
||||||
|
|
||||||
`vulnSeveritySources` is a list of source names in priority order (for example `auto`, `nvd`, or vendor IDs such as `redhat`, `alpine`). If omitted, zot defaults it to `["auto"]`, consistent with the Trivy CLI. See [Trivy: severity selection](https://trivy.dev/docs/latest/scanner/vulnerability/#severity-selection).
|
`vulnSeveritySources` is a list of source names in priority order (for example `auto`, `nvd`, or vendor IDs such as `redhat`, `alpine`). If omitted, zot defaults it to `["auto"]`, consistent with the Trivy CLI. See [Trivy: severity selection](https://trivy.dev/docs/latest/scanner/vulnerability/#severity-selection).
|
||||||
|
|
||||||
|
`sbom.enable` lets zot generate SBOMs while scanning and store them as OCI artifacts attached to the scanned image. `sbom.format` supports `spdx-json` (default) and `cyclonedx`.
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"distSpecVersion": "1.1.1",
|
||||||
|
"storage": {
|
||||||
|
"rootDirectory": "/tmp/zot"
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"address": "127.0.0.1",
|
||||||
|
"port": "8080"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"level": "debug",
|
||||||
|
"output": "/tmp/zot.log"
|
||||||
|
},
|
||||||
|
"extensions": {
|
||||||
|
"ui": {
|
||||||
|
"enable": true
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"enable": true,
|
||||||
|
"cve": {
|
||||||
|
"updateInterval": "24h",
|
||||||
|
"trivy": {
|
||||||
|
"dbRepository": "ghcr.io/aquasecurity/trivy-db",
|
||||||
|
"javaDBRepository": "ghcr.io/aquasecurity/trivy-java-db",
|
||||||
|
"vulnSeveritySources": ["auto"],
|
||||||
|
"sbom": {
|
||||||
|
"enable": true,
|
||||||
|
"format": "spdx-json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ func TestConfigReloader(t *testing.T) {
|
|||||||
|
|
||||||
defer func() { os.Args = oldArgs }()
|
defer func() { os.Args = oldArgs }()
|
||||||
|
|
||||||
Convey("reload access control config", t, func(c C) {
|
Convey("reload access control config", t, func(conveyCtx C) {
|
||||||
port := test.GetFreePort()
|
port := test.GetFreePort()
|
||||||
baseURL := test.GetBaseURL(port)
|
baseURL := test.GetBaseURL(port)
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ func TestConfigReloader(t *testing.T) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err = cli.NewServerRootCmd().Execute()
|
err = cli.NewServerRootCmd().Execute()
|
||||||
So(err, ShouldBeNil)
|
conveyCtx.So(err, ShouldBeNil)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
test.WaitTillServerReady(baseURL)
|
test.WaitTillServerReady(baseURL)
|
||||||
@@ -173,7 +173,7 @@ func TestConfigReloader(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("reload gc config", t, func(c C) {
|
Convey("reload gc config", t, func(ctx C) {
|
||||||
port := test.GetFreePort()
|
port := test.GetFreePort()
|
||||||
baseURL := test.GetBaseURL(port)
|
baseURL := test.GetBaseURL(port)
|
||||||
|
|
||||||
@@ -214,7 +214,7 @@ func TestConfigReloader(t *testing.T) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err = cli.NewServerRootCmd().Execute()
|
err = cli.NewServerRootCmd().Execute()
|
||||||
So(err, ShouldBeNil)
|
ctx.So(err, ShouldBeNil)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
test.WaitTillServerReady(baseURL)
|
test.WaitTillServerReady(baseURL)
|
||||||
@@ -300,7 +300,7 @@ func TestConfigReloader(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("reload sync config", t, func(c C) {
|
Convey("reload sync config", t, func(ctx C) {
|
||||||
port := test.GetFreePort()
|
port := test.GetFreePort()
|
||||||
baseURL := test.GetBaseURL(port)
|
baseURL := test.GetBaseURL(port)
|
||||||
|
|
||||||
@@ -352,7 +352,7 @@ func TestConfigReloader(t *testing.T) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err = cli.NewServerRootCmd().Execute()
|
err = cli.NewServerRootCmd().Execute()
|
||||||
So(err, ShouldBeNil)
|
ctx.So(err, ShouldBeNil)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
test.WaitTillServerReady(baseURL)
|
test.WaitTillServerReady(baseURL)
|
||||||
@@ -450,7 +450,7 @@ func TestConfigReloader(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("reload scrub and CVE config", t, func(c C) {
|
Convey("reload scrub and CVE config", t, func(ctx C) {
|
||||||
port := test.GetFreePort()
|
port := test.GetFreePort()
|
||||||
baseURL := test.GetBaseURL(port)
|
baseURL := test.GetBaseURL(port)
|
||||||
|
|
||||||
@@ -495,7 +495,7 @@ func TestConfigReloader(t *testing.T) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err = cli.NewServerRootCmd().Execute()
|
err = cli.NewServerRootCmd().Execute()
|
||||||
So(err, ShouldBeNil)
|
ctx.So(err, ShouldBeNil)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
test.WaitTillServerReady(baseURL)
|
test.WaitTillServerReady(baseURL)
|
||||||
@@ -591,7 +591,7 @@ func TestConfigReloader(t *testing.T) {
|
|||||||
So(found, ShouldBeTrue)
|
So(found, ShouldBeTrue)
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("reload bad config", t, func(c C) {
|
Convey("reload bad config", t, func(conveyCtx C) {
|
||||||
port := test.GetFreePort()
|
port := test.GetFreePort()
|
||||||
baseURL := test.GetBaseURL(port)
|
baseURL := test.GetBaseURL(port)
|
||||||
|
|
||||||
@@ -643,7 +643,7 @@ func TestConfigReloader(t *testing.T) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err = cli.NewServerRootCmd().Execute()
|
err = cli.NewServerRootCmd().Execute()
|
||||||
So(err, ShouldBeNil)
|
conveyCtx.So(err, ShouldBeNil)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
test.WaitTillServerReady(baseURL)
|
test.WaitTillServerReady(baseURL)
|
||||||
|
|||||||
@@ -968,7 +968,7 @@ func TestServeSearchEnabledDefaultCVEDB(t *testing.T) {
|
|||||||
// The default config handling logic will convert the 1h interval to a 2h interval
|
// The default config handling logic will convert the 1h interval to a 2h interval
|
||||||
substring := "\"Search\":{\"Enable\":true,\"CVE\":{\"UpdateInterval\":7200000000000,\"Trivy\":" +
|
substring := "\"Search\":{\"Enable\":true,\"CVE\":{\"UpdateInterval\":7200000000000,\"Trivy\":" +
|
||||||
"{\"DBRepository\":\"ghcr.io/aquasecurity/trivy-db\",\"JavaDBRepository\":\"ghcr.io/aquasecurity/trivy-java-db\"," +
|
"{\"DBRepository\":\"ghcr.io/aquasecurity/trivy-db\",\"JavaDBRepository\":\"ghcr.io/aquasecurity/trivy-java-db\"," +
|
||||||
"\"VulnSeveritySources\":[\"auto\"]}}}"
|
"\"VulnSeveritySources\":[\"auto\"],\"SBOM\":null}}}"
|
||||||
|
|
||||||
found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)
|
found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ type TrivyConfig struct {
|
|||||||
// VulnSeveritySources controls Trivy's severity source selection (same as Trivy's --vuln-severity-source).
|
// VulnSeveritySources controls Trivy's severity source selection (same as Trivy's --vuln-severity-source).
|
||||||
// If empty, zot will default it to ["auto"].
|
// If empty, zot will default it to ["auto"].
|
||||||
VulnSeveritySources []string
|
VulnSeveritySources []string
|
||||||
|
SBOM *SBOMConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type SBOMConfig struct {
|
||||||
|
Enable bool
|
||||||
|
// Format controls the generated SBOM output format.
|
||||||
|
// Supported values are "spdx-json" (default) and "cyclonedx".
|
||||||
|
Format string
|
||||||
}
|
}
|
||||||
|
|
||||||
type MetricsConfig struct {
|
type MetricsConfig struct {
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package trivy
|
package trivy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"maps"
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -24,6 +28,7 @@ import (
|
|||||||
"github.com/google/go-containerregistry/pkg/name"
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
regTypes "github.com/google/go-containerregistry/pkg/v1/types"
|
regTypes "github.com/google/go-containerregistry/pkg/v1/types"
|
||||||
godigest "github.com/opencontainers/go-digest"
|
godigest "github.com/opencontainers/go-digest"
|
||||||
|
"github.com/opencontainers/image-spec/specs-go"
|
||||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
|
|
||||||
@@ -34,16 +39,28 @@ import (
|
|||||||
cvecache "zotregistry.dev/zot/v2/pkg/extensions/search/cve/cache"
|
cvecache "zotregistry.dev/zot/v2/pkg/extensions/search/cve/cache"
|
||||||
cvemodel "zotregistry.dev/zot/v2/pkg/extensions/search/cve/model"
|
cvemodel "zotregistry.dev/zot/v2/pkg/extensions/search/cve/model"
|
||||||
"zotregistry.dev/zot/v2/pkg/log"
|
"zotregistry.dev/zot/v2/pkg/log"
|
||||||
|
"zotregistry.dev/zot/v2/pkg/meta"
|
||||||
mTypes "zotregistry.dev/zot/v2/pkg/meta/types"
|
mTypes "zotregistry.dev/zot/v2/pkg/meta/types"
|
||||||
"zotregistry.dev/zot/v2/pkg/storage"
|
"zotregistry.dev/zot/v2/pkg/storage"
|
||||||
|
storageConstants "zotregistry.dev/zot/v2/pkg/storage/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
const cacheSize = 1000000
|
const cacheSize = 1000000
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultSBOMFormat = types.FormatSPDXJSON
|
||||||
|
defaultSBOMArtifactType = "application/spdx+json"
|
||||||
|
defaultSBOMLayerMediaType = "application/spdx+json"
|
||||||
|
cycloneDXArtifactType = "application/vnd.cyclonedx+json"
|
||||||
|
cycloneDXLayerMediaType = "application/vnd.cyclonedx+json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errImageStoreNotFound = errors.New("image store not found")
|
||||||
|
|
||||||
// getNewScanOptions sets trivy configuration values for our scans and returns them as
|
// getNewScanOptions sets trivy configuration values for our scans and returns them as
|
||||||
// a trivy Options structure.
|
// a trivy Options structure.
|
||||||
func getNewScanOptions(dir string, dbRepositoryRef, javaDBRepositoryRef name.Reference,
|
func getNewScanOptions(dir string, dbRepositoryRef, javaDBRepositoryRef name.Reference,
|
||||||
vulnSeveritySources []dbTypes.SourceID,
|
vulnSeveritySources []dbTypes.SourceID, sbomEnabled bool,
|
||||||
) *flag.Options {
|
) *flag.Options {
|
||||||
scanOptions := flag.Options{
|
scanOptions := flag.Options{
|
||||||
GlobalOptions: flag.GlobalOptions{
|
GlobalOptions: flag.GlobalOptions{
|
||||||
@@ -80,6 +97,14 @@ func getNewScanOptions(dir string, dbRepositoryRef, javaDBRepositoryRef name.Ref
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sbomEnabled {
|
||||||
|
scanOptions.ScanOptions.Scanners = types.Scanners{types.VulnerabilityScanner, types.LicenseScanner}
|
||||||
|
scanOptions.ScanOptions.DetectionPriority = fanalTypes.PriorityComprehensive
|
||||||
|
scanOptions.ImageOptions.ScanRemovedPkgs = true
|
||||||
|
scanOptions.LicenseOptions.LicenseFull = true
|
||||||
|
scanOptions.PackageOptions.IncludeDevDeps = true
|
||||||
|
}
|
||||||
|
|
||||||
return &scanOptions
|
return &scanOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +123,20 @@ type Scanner struct {
|
|||||||
dbRepositoryRef name.Reference
|
dbRepositoryRef name.Reference
|
||||||
javaDBRepositoryRef name.Reference
|
javaDBRepositoryRef name.Reference
|
||||||
vulnSeveritySources []dbTypes.SourceID
|
vulnSeveritySources []dbTypes.SourceID
|
||||||
|
sbomOptions sbomOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
type sbomOptions struct {
|
||||||
|
enabled bool
|
||||||
|
reportFormat types.Format
|
||||||
|
artifactType string
|
||||||
|
layerMediaType string
|
||||||
|
}
|
||||||
|
|
||||||
|
type generatedSBOM struct {
|
||||||
|
filePath string
|
||||||
|
digest godigest.Digest
|
||||||
|
size int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewScanner(storeController storage.StoreController,
|
func NewScanner(storeController storage.StoreController,
|
||||||
@@ -112,6 +151,8 @@ func NewScanner(storeController storage.StoreController,
|
|||||||
trivyCfg = &extconf.TrivyConfig{}
|
trivyCfg = &extconf.TrivyConfig{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sbomOpts := getSBOMOptions(trivyCfg.SBOM, log)
|
||||||
|
|
||||||
dbRepository := trivyCfg.DBRepository
|
dbRepository := trivyCfg.DBRepository
|
||||||
javaDBRepository := trivyCfg.JavaDBRepository
|
javaDBRepository := trivyCfg.JavaDBRepository
|
||||||
vulnSeveritySources := trivyCfg.VulnSeveritySources
|
vulnSeveritySources := trivyCfg.VulnSeveritySources
|
||||||
@@ -158,7 +199,7 @@ func NewScanner(storeController storage.StoreController,
|
|||||||
rootDir := imageStore.RootDir()
|
rootDir := imageStore.RootDir()
|
||||||
|
|
||||||
cacheDir := path.Join(rootDir, "_trivy")
|
cacheDir := path.Join(rootDir, "_trivy")
|
||||||
opts := getNewScanOptions(cacheDir, dbRepositoryRef, javaDBRepositoryRef, sevSources)
|
opts := getNewScanOptions(cacheDir, dbRepositoryRef, javaDBRepositoryRef, sevSources, sbomOpts.enabled)
|
||||||
|
|
||||||
cveController.DefaultCveConfig = opts
|
cveController.DefaultCveConfig = opts
|
||||||
}
|
}
|
||||||
@@ -168,7 +209,7 @@ func NewScanner(storeController storage.StoreController,
|
|||||||
rootDir := storage.RootDir()
|
rootDir := storage.RootDir()
|
||||||
|
|
||||||
cacheDir := path.Join(rootDir, "_trivy")
|
cacheDir := path.Join(rootDir, "_trivy")
|
||||||
opts := getNewScanOptions(cacheDir, dbRepositoryRef, javaDBRepositoryRef, sevSources)
|
opts := getNewScanOptions(cacheDir, dbRepositoryRef, javaDBRepositoryRef, sevSources, sbomOpts.enabled)
|
||||||
|
|
||||||
subCveConfig[route] = opts
|
subCveConfig[route] = opts
|
||||||
}
|
}
|
||||||
@@ -186,9 +227,43 @@ func NewScanner(storeController storage.StoreController,
|
|||||||
dbRepositoryRef: dbRepositoryRef,
|
dbRepositoryRef: dbRepositoryRef,
|
||||||
javaDBRepositoryRef: javaDBRepositoryRef,
|
javaDBRepositoryRef: javaDBRepositoryRef,
|
||||||
vulnSeveritySources: sevSources,
|
vulnSeveritySources: sevSources,
|
||||||
|
sbomOptions: sbomOpts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSBOMOptions(cfg *extconf.SBOMConfig, logger log.Logger) sbomOptions {
|
||||||
|
options := sbomOptions{
|
||||||
|
enabled: false,
|
||||||
|
reportFormat: defaultSBOMFormat,
|
||||||
|
artifactType: defaultSBOMArtifactType,
|
||||||
|
layerMediaType: defaultSBOMLayerMediaType,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg == nil || !cfg.Enable {
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
options.enabled = true
|
||||||
|
|
||||||
|
format := strings.ToLower(cfg.Format)
|
||||||
|
if format == "" || format == string(types.FormatSPDXJSON) {
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
if format == string(types.FormatCycloneDX) {
|
||||||
|
options.reportFormat = types.FormatCycloneDX
|
||||||
|
options.artifactType = cycloneDXArtifactType
|
||||||
|
options.layerMediaType = cycloneDXLayerMediaType
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Warn().Str("format", cfg.Format).
|
||||||
|
Msg("unsupported trivy sbom format, defaulting to spdx-json")
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
func (scanner Scanner) getTrivyOptions(image string) flag.Options {
|
func (scanner Scanner) getTrivyOptions(image string) flag.Options {
|
||||||
// Split image to get route prefix
|
// Split image to get route prefix
|
||||||
prefixName := storage.GetRoutePrefix(image)
|
prefixName := storage.GetRoutePrefix(image)
|
||||||
@@ -244,13 +319,16 @@ func (scanner Scanner) withTempDir(wrappedFunc func() error) error {
|
|||||||
return wrappedFunc()
|
return wrappedFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (scanner Scanner) runTrivy(ctx context.Context, opts flag.Options) (types.Report, error) {
|
func (scanner Scanner) runTrivy(ctx context.Context, opts flag.Options) (types.Report, *generatedSBOM, error) {
|
||||||
err := scanner.checkDBPresence()
|
err := scanner.checkDBPresence()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.Report{}, err
|
return types.Report{}, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
report := types.Report{}
|
report := types.Report{}
|
||||||
|
|
||||||
|
var sbom *generatedSBOM
|
||||||
|
|
||||||
err = scanner.withTempDir(func() error {
|
err = scanner.withTempDir(func() error {
|
||||||
runner, err := artifact.NewRunner(ctx, opts, artifact.TargetContainerImage)
|
runner, err := artifact.NewRunner(ctx, opts, artifact.TargetContainerImage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -264,11 +342,74 @@ func (scanner Scanner) runTrivy(ctx context.Context, opts flag.Options) (types.R
|
|||||||
}
|
}
|
||||||
|
|
||||||
report, err = runner.Filter(ctx, opts, report)
|
report, err = runner.Filter(ctx, opts, report)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if scanner.sbomOptions.enabled {
|
||||||
|
sbom, err = scanner.generateSBOM(ctx, runner, opts, report)
|
||||||
|
if err != nil {
|
||||||
|
scanner.log.Warn().Err(err).Str("image", opts.ImageOptions.Input).Msg("failed to generate sbom")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
|
||||||
return report, err
|
return report, sbom, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (scanner Scanner) generateSBOM(ctx context.Context, runner artifact.Runner, opts flag.Options, report types.Report,
|
||||||
|
) (*generatedSBOM, error) {
|
||||||
|
sbomTempFile, err := os.CreateTemp("", "zot-trivy-sbom-*.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sbomPath := sbomTempFile.Name()
|
||||||
|
|
||||||
|
if err = sbomTempFile.Close(); err != nil {
|
||||||
|
_ = os.Remove(sbomPath)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sbomOpts := opts
|
||||||
|
sbomOpts.ReportOptions.Format = scanner.sbomOptions.reportFormat
|
||||||
|
sbomOpts.ReportOptions.Output = sbomTempFile.Name()
|
||||||
|
sbomOpts.ReportOptions.ListAllPkgs = true
|
||||||
|
sbomOpts.ReportOptions.DependencyTree = true
|
||||||
|
|
||||||
|
if err = runner.Report(ctx, sbomOpts, report); err != nil {
|
||||||
|
_ = os.Remove(sbomPath)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sbomDigest, sbomSize, err := digestAndSizeFromFile(sbomPath)
|
||||||
|
if err != nil {
|
||||||
|
_ = os.Remove(sbomPath)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &generatedSBOM{filePath: sbomPath, digest: sbomDigest, size: sbomSize}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func digestAndSizeFromFile(filePath string) (godigest.Digest, int64, error) {
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
digester := godigest.Canonical.Digester()
|
||||||
|
size, err := io.Copy(digester.Hash(), file)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return digester.Digest(), size, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (scanner Scanner) IsImageFormatScannable(repo, ref string) (bool, error) {
|
func (scanner Scanner) IsImageFormatScannable(repo, ref string) (bool, error) {
|
||||||
@@ -464,13 +605,22 @@ func (scanner Scanner) scanManifest(ctx context.Context, repo, digest string) (m
|
|||||||
|
|
||||||
scanner.dbLock.Lock()
|
scanner.dbLock.Lock()
|
||||||
opts := scanner.getTrivyOptions(image)
|
opts := scanner.getTrivyOptions(image)
|
||||||
report, err := scanner.runTrivy(ctx, opts)
|
report, sbom, err := scanner.runTrivy(ctx, opts)
|
||||||
scanner.dbLock.Unlock()
|
scanner.dbLock.Unlock()
|
||||||
|
if sbom != nil && sbom.filePath != "" {
|
||||||
|
defer os.Remove(sbom.filePath)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil { //nolint: wsl
|
if err != nil { //nolint: wsl
|
||||||
return cveidMap, err
|
return cveidMap, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SBOM persistence is best-effort: CVE scanning should still complete even if
|
||||||
|
// SBOM artifact upload fails.
|
||||||
|
if err = scanner.storeSBOMAsOCIArtifact(ctx, repo, digest, sbom); err != nil {
|
||||||
|
scanner.log.Warn().Err(err).Str("image", image).Msg("failed to store generated sbom as OCI artifact")
|
||||||
|
}
|
||||||
|
|
||||||
for _, result := range report.Results {
|
for _, result := range report.Results {
|
||||||
for _, vulnerability := range result.Vulnerabilities {
|
for _, vulnerability := range result.Vulnerabilities {
|
||||||
pkgName := vulnerability.PkgName
|
pkgName := vulnerability.PkgName
|
||||||
@@ -540,6 +690,148 @@ func (scanner Scanner) scanManifest(ctx context.Context, repo, digest string) (m
|
|||||||
return cveidMap, nil
|
return cveidMap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (scanner Scanner) storeSBOMAsOCIArtifact(ctx context.Context,
|
||||||
|
repo, subjectDigest string, sbom *generatedSBOM,
|
||||||
|
) error {
|
||||||
|
if !scanner.sbomOptions.enabled {
|
||||||
|
scanner.log.Debug().Str("repo", repo).Str("subject", subjectDigest).
|
||||||
|
Msg("skipping sbom artifact persistence: sbom support disabled")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if sbom == nil || sbom.filePath == "" {
|
||||||
|
scanner.log.Debug().Str("repo", repo).Str("subject", subjectDigest).
|
||||||
|
Msg("skipping sbom artifact persistence: no sbom file available")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
imgStore := scanner.storeController.GetImageStore(repo)
|
||||||
|
if imgStore == nil {
|
||||||
|
scanner.log.Warn().Str("repo", repo).Msg("skipping sbom artifact persistence: image store not found")
|
||||||
|
|
||||||
|
return fmt.Errorf("%w for repo %q", errImageStoreNotFound, repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
subject := godigest.Digest(subjectDigest)
|
||||||
|
if err := subject.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
subjectManifestBlob, _, subjectMediaType, err := imgStore.GetImageManifest(repo, subjectDigest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
referrers, err := imgStore.GetReferrers(repo, subject, []string{scanner.sbomOptions.artifactType})
|
||||||
|
if err == nil && len(referrers.Manifests) > 0 {
|
||||||
|
scanner.log.Debug().Str("repo", repo).Str("subject", subjectDigest).
|
||||||
|
Str("artifactType", scanner.sbomOptions.artifactType).
|
||||||
|
Msg("skipping sbom artifact persistence: referrer already exists")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sbomExists, _, err := imgStore.CheckBlob(repo, sbom.digest)
|
||||||
|
if err != nil && !errors.Is(err, zerr.ErrBlobNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sbomExists {
|
||||||
|
sbomFile, openErr := os.Open(sbom.filePath)
|
||||||
|
if openErr != nil {
|
||||||
|
return openErr
|
||||||
|
}
|
||||||
|
|
||||||
|
defer sbomFile.Close()
|
||||||
|
|
||||||
|
if _, _, err = imgStore.FullBlobUpload(repo, sbomFile, sbom.digest); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner.log.Debug().Str("repo", repo).Str("subject", subjectDigest).Str("digest", sbom.digest.String()).
|
||||||
|
Msg("uploaded sbom blob")
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyConfigExists, _, err := imgStore.CheckBlob(repo, ispec.DescriptorEmptyJSON.Digest)
|
||||||
|
if err != nil && !errors.Is(err, zerr.ErrBlobNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !emptyConfigExists {
|
||||||
|
if _, _, err = imgStore.FullBlobUpload(repo, bytes.NewReader(ispec.DescriptorEmptyJSON.Data),
|
||||||
|
ispec.DescriptorEmptyJSON.Digest); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner.log.Debug().Str("repo", repo).Str("digest", ispec.DescriptorEmptyJSON.Digest.String()).
|
||||||
|
Msg("uploaded empty config blob for sbom artifact")
|
||||||
|
}
|
||||||
|
|
||||||
|
sbomManifest := ispec.Manifest{
|
||||||
|
Versioned: specs.Versioned{SchemaVersion: storageConstants.SchemaVersion},
|
||||||
|
MediaType: ispec.MediaTypeImageManifest,
|
||||||
|
ArtifactType: scanner.sbomOptions.artifactType,
|
||||||
|
Subject: &ispec.Descriptor{
|
||||||
|
MediaType: subjectMediaType,
|
||||||
|
Digest: subject,
|
||||||
|
Size: int64(len(subjectManifestBlob)),
|
||||||
|
},
|
||||||
|
Config: ispec.Descriptor{
|
||||||
|
MediaType: ispec.MediaTypeEmptyJSON,
|
||||||
|
Digest: ispec.DescriptorEmptyJSON.Digest,
|
||||||
|
Size: int64(len(ispec.DescriptorEmptyJSON.Data)),
|
||||||
|
},
|
||||||
|
Layers: []ispec.Descriptor{
|
||||||
|
{
|
||||||
|
MediaType: scanner.sbomOptions.layerMediaType,
|
||||||
|
Digest: sbom.digest,
|
||||||
|
Size: sbom.size,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sbomManifestBlob, err := json.Marshal(sbomManifest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sbomManifestDigest := godigest.FromBytes(sbomManifestBlob)
|
||||||
|
_, _, err = imgStore.PutImageManifest(repo, sbomManifestDigest.String(), ispec.MediaTypeImageManifest,
|
||||||
|
sbomManifestBlob, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner.log.Debug().Str("repo", repo).Str("subject", subjectDigest).
|
||||||
|
Str("manifestDigest", sbomManifestDigest.String()).
|
||||||
|
Str("artifactType", scanner.sbomOptions.artifactType).
|
||||||
|
Msg("stored sbom as oci artifact manifest")
|
||||||
|
|
||||||
|
if scanner.metaDB != nil {
|
||||||
|
err = meta.OnUpdateManifest(ctx, repo, sbomManifestDigest.String(),
|
||||||
|
ispec.MediaTypeImageManifest, sbomManifestDigest, sbomManifestBlob,
|
||||||
|
scanner.storeController, scanner.metaDB, scanner.log)
|
||||||
|
if err != nil {
|
||||||
|
scanner.log.Warn().Err(err).Str("repo", repo).Str("subject", subjectDigest).
|
||||||
|
Str("manifestDigest", sbomManifestDigest.String()).
|
||||||
|
Msg("failed to persist sbom artifact metadata")
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner.log.Debug().Str("repo", repo).Str("subject", subjectDigest).
|
||||||
|
Str("manifestDigest", sbomManifestDigest.String()).
|
||||||
|
Msg("persisted sbom artifact metadata")
|
||||||
|
} else {
|
||||||
|
scanner.log.Debug().Str("repo", repo).Str("subject", subjectDigest).
|
||||||
|
Msg("skipping sbom artifact metadata persistence: metadb not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getCVEReference(primaryURL string, references []string) string {
|
func getCVEReference(primaryURL string, references []string) string {
|
||||||
if primaryURL != "" {
|
if primaryURL != "" {
|
||||||
return primaryURL
|
return primaryURL
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ package trivy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/commands/artifact"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/flag"
|
||||||
|
trivyTypes "github.com/aquasecurity/trivy/pkg/types"
|
||||||
godigest "github.com/opencontainers/go-digest"
|
godigest "github.com/opencontainers/go-digest"
|
||||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
@@ -33,6 +37,52 @@ import (
|
|||||||
"zotregistry.dev/zot/v2/pkg/test/mocks"
|
"zotregistry.dev/zot/v2/pkg/test/mocks"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type fakeArtifactRunner struct {
|
||||||
|
reportFn func(ctx context.Context, opts flag.Options, report trivyTypes.Report) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeArtifactRunner) ScanImage(ctx context.Context, opts flag.Options) (trivyTypes.Report, error) {
|
||||||
|
return trivyTypes.Report{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeArtifactRunner) ScanFilesystem(ctx context.Context, opts flag.Options) (trivyTypes.Report, error) {
|
||||||
|
return trivyTypes.Report{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeArtifactRunner) ScanRootfs(ctx context.Context, opts flag.Options) (trivyTypes.Report, error) {
|
||||||
|
return trivyTypes.Report{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeArtifactRunner) ScanRepository(ctx context.Context, opts flag.Options) (trivyTypes.Report, error) {
|
||||||
|
return trivyTypes.Report{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeArtifactRunner) ScanSBOM(ctx context.Context, opts flag.Options) (trivyTypes.Report, error) {
|
||||||
|
return trivyTypes.Report{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeArtifactRunner) ScanVM(ctx context.Context, opts flag.Options) (trivyTypes.Report, error) {
|
||||||
|
return trivyTypes.Report{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeArtifactRunner) Filter(ctx context.Context, opts flag.Options, report trivyTypes.Report) (trivyTypes.Report, error) {
|
||||||
|
return report, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeArtifactRunner) Report(ctx context.Context, opts flag.Options, report trivyTypes.Report) error {
|
||||||
|
if f.reportFn != nil {
|
||||||
|
return f.reportFn(ctx, opts, report)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeArtifactRunner) Close(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ artifact.Runner = fakeArtifactRunner{}
|
||||||
|
|
||||||
func generateTestImage(storeController storage.StoreController, imageName string) {
|
func generateTestImage(storeController storage.StoreController, imageName string) {
|
||||||
repoName, tag := common.GetImageDirAndTag(imageName)
|
repoName, tag := common.GetImageDirAndTag(imageName)
|
||||||
|
|
||||||
@@ -43,6 +93,45 @@ func generateTestImage(storeController storage.StoreController, imageName string
|
|||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGenerateSBOM(t *testing.T) {
|
||||||
|
Convey("generateSBOM writes report to file and returns digest metadata", t, func() {
|
||||||
|
logger := log.NewTestLogger()
|
||||||
|
scanner := Scanner{
|
||||||
|
log: logger,
|
||||||
|
sbomOptions: sbomOptions{
|
||||||
|
enabled: true,
|
||||||
|
reportFormat: trivyTypes.FormatSPDXJSON,
|
||||||
|
artifactType: defaultSBOMArtifactType,
|
||||||
|
layerMediaType: defaultSBOMLayerMediaType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedSBOM := []byte(`{"spdxVersion":"SPDX-2.3"}`)
|
||||||
|
mockRunner := fakeArtifactRunner{
|
||||||
|
reportFn: func(ctx context.Context, opts flag.Options, report trivyTypes.Report) error {
|
||||||
|
So(opts.ReportOptions.Output, ShouldNotEqual, "")
|
||||||
|
So(opts.ReportOptions.Format, ShouldEqual, trivyTypes.FormatSPDXJSON)
|
||||||
|
So(opts.ReportOptions.ListAllPkgs, ShouldBeTrue)
|
||||||
|
So(opts.ReportOptions.DependencyTree, ShouldBeTrue)
|
||||||
|
|
||||||
|
return os.WriteFile(opts.ReportOptions.Output, expectedSBOM, 0o600)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
generated, err := scanner.generateSBOM(context.Background(), mockRunner, flag.Options{}, trivyTypes.Report{})
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(generated, ShouldNotBeNil)
|
||||||
|
So(generated.filePath, ShouldNotEqual, "")
|
||||||
|
defer os.Remove(generated.filePath)
|
||||||
|
|
||||||
|
storedSBOM, err := os.ReadFile(generated.filePath)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(storedSBOM, ShouldResemble, expectedSBOM)
|
||||||
|
So(generated.size, ShouldEqual, int64(len(expectedSBOM)))
|
||||||
|
So(generated.digest, ShouldEqual, godigest.FromBytes(expectedSBOM))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestMultipleStoragePath(t *testing.T) {
|
func TestMultipleStoragePath(t *testing.T) {
|
||||||
Convey("Test multiple storage path", t, func() {
|
Convey("Test multiple storage path", t, func() {
|
||||||
// Create temporary directory
|
// Create temporary directory
|
||||||
@@ -214,7 +303,7 @@ func TestTrivyLibraryErrors(t *testing.T) {
|
|||||||
|
|
||||||
// Try to scan without a valid DB being downloaded
|
// Try to scan without a valid DB being downloaded
|
||||||
opts := scanner.getTrivyOptions(img)
|
opts := scanner.getTrivyOptions(img)
|
||||||
_, err = scanner.runTrivy(ctx, opts)
|
_, _, err = scanner.runTrivy(ctx, opts)
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
So(err, ShouldWrap, zerr.ErrCVEDBNotFound)
|
So(err, ShouldWrap, zerr.ErrCVEDBNotFound)
|
||||||
|
|
||||||
@@ -243,25 +332,25 @@ func TestTrivyLibraryErrors(t *testing.T) {
|
|||||||
|
|
||||||
// Scanning image with correct options
|
// Scanning image with correct options
|
||||||
opts = scanner.getTrivyOptions(img)
|
opts = scanner.getTrivyOptions(img)
|
||||||
_, err = scanner.runTrivy(ctx, opts)
|
_, _, err = scanner.runTrivy(ctx, opts)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
// Scanning image with incorrect cache options
|
// Scanning image with incorrect cache options
|
||||||
// to trigger runner initialization errors
|
// to trigger runner initialization errors
|
||||||
opts.CacheOptions.CacheBackend = "redis://asdf!$%&!*)("
|
opts.CacheOptions.CacheBackend = "redis://asdf!$%&!*)("
|
||||||
_, err = scanner.runTrivy(ctx, opts)
|
_, _, err = scanner.runTrivy(ctx, opts)
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
|
|
||||||
// Scanning image with invalid input to trigger a scanner error
|
// Scanning image with invalid input to trigger a scanner error
|
||||||
opts = scanner.getTrivyOptions("nilnonexisting_image:0.0.1")
|
opts = scanner.getTrivyOptions("nilnonexisting_image:0.0.1")
|
||||||
_, err = scanner.runTrivy(ctx, opts)
|
_, _, err = scanner.runTrivy(ctx, opts)
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
|
|
||||||
// Scanning image with incorrect report options
|
// Scanning image with incorrect report options
|
||||||
// to trigger report filtering errors
|
// to trigger report filtering errors
|
||||||
opts = scanner.getTrivyOptions(img)
|
opts = scanner.getTrivyOptions(img)
|
||||||
opts.ReportOptions.IgnorePolicy = "invalid file path"
|
opts.ReportOptions.IgnorePolicy = "invalid file path"
|
||||||
_, err = scanner.runTrivy(ctx, opts)
|
_, _, err = scanner.runTrivy(ctx, opts)
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -433,14 +522,14 @@ func TestTrivyDBUrl(t *testing.T) {
|
|||||||
img := "zot-test:0.0.1" //nolint:goconst
|
img := "zot-test:0.0.1" //nolint:goconst
|
||||||
|
|
||||||
opts := scanner.getTrivyOptions(img)
|
opts := scanner.getTrivyOptions(img)
|
||||||
_, err = scanner.runTrivy(ctx, opts)
|
_, _, err = scanner.runTrivy(ctx, opts)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
// Scanning image containing a jar file
|
// Scanning image containing a jar file
|
||||||
img = "zot-cve-java-test:0.0.1"
|
img = "zot-cve-java-test:0.0.1"
|
||||||
|
|
||||||
opts = scanner.getTrivyOptions(img)
|
opts = scanner.getTrivyOptions(img)
|
||||||
_, err = scanner.runTrivy(ctx, opts)
|
_, _, err = scanner.runTrivy(ctx, opts)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -527,6 +616,122 @@ func TestVulnSeveritySourcesDefaulting(t *testing.T) {
|
|||||||
So(scanner, ShouldNotBeNil)
|
So(scanner, ShouldNotBeNil)
|
||||||
So(scanner.vulnSeveritySources, ShouldResemble, []dbTypes.SourceID{"nvd", "ghsa"})
|
So(scanner.vulnSeveritySources, ShouldResemble, []dbTypes.SourceID{"nvd", "ghsa"})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("NewScanner enables SBOM generation with default options", t, func() {
|
||||||
|
scanner := NewScanner(storage.StoreController{}, nil, &extconf.CVEConfig{
|
||||||
|
Trivy: &extconf.TrivyConfig{
|
||||||
|
DBRepository: "ghcr.io/project-zot/trivy-db",
|
||||||
|
SBOM: &extconf.SBOMConfig{
|
||||||
|
Enable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, log.NewTestLogger())
|
||||||
|
So(scanner, ShouldNotBeNil)
|
||||||
|
So(scanner.sbomOptions.enabled, ShouldBeTrue)
|
||||||
|
So(scanner.sbomOptions.reportFormat, ShouldEqual, trivyTypes.FormatSPDXJSON)
|
||||||
|
So(scanner.sbomOptions.artifactType, ShouldEqual, defaultSBOMArtifactType)
|
||||||
|
So(scanner.sbomOptions.layerMediaType, ShouldEqual, defaultSBOMLayerMediaType)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("NewScanner supports CycloneDX SBOM format", t, func() {
|
||||||
|
scanner := NewScanner(storage.StoreController{}, nil, &extconf.CVEConfig{
|
||||||
|
Trivy: &extconf.TrivyConfig{
|
||||||
|
DBRepository: "ghcr.io/project-zot/trivy-db",
|
||||||
|
SBOM: &extconf.SBOMConfig{
|
||||||
|
Enable: true,
|
||||||
|
Format: string(trivyTypes.FormatCycloneDX),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, log.NewTestLogger())
|
||||||
|
So(scanner, ShouldNotBeNil)
|
||||||
|
So(scanner.sbomOptions.reportFormat, ShouldEqual, trivyTypes.FormatCycloneDX)
|
||||||
|
So(scanner.sbomOptions.artifactType, ShouldEqual, cycloneDXArtifactType)
|
||||||
|
So(scanner.sbomOptions.layerMediaType, ShouldEqual, cycloneDXLayerMediaType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreSBOMAsOCIArtifact(t *testing.T) {
|
||||||
|
Convey("storeSBOMAsOCIArtifact stores SBOM once as OCI referrer", t, func() {
|
||||||
|
rootDir := t.TempDir()
|
||||||
|
|
||||||
|
logger := log.NewTestLogger()
|
||||||
|
metrics := monitoring.NewMetricsServer(false, logger)
|
||||||
|
defer metrics.Stop()
|
||||||
|
store := local.NewImageStore(rootDir, false, false, logger, metrics, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
storeController := storage.StoreController{
|
||||||
|
DefaultStore: store,
|
||||||
|
}
|
||||||
|
|
||||||
|
params := boltdb.DBParameters{RootDir: rootDir}
|
||||||
|
boltDriver, err := boltdb.GetBoltDriver(params)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
metaDB, err := boltdb.New(boltDriver, logger)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
generateTestImage(storeController, "repo:1.0")
|
||||||
|
|
||||||
|
_, subjectDigest, _, err := store.GetImageManifest("repo", "1.0")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
scanner := NewScanner(storeController, metaDB, &extconf.CVEConfig{
|
||||||
|
Trivy: &extconf.TrivyConfig{
|
||||||
|
DBRepository: "ghcr.io/project-zot/trivy-db",
|
||||||
|
SBOM: &extconf.SBOMConfig{
|
||||||
|
Enable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, logger)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
sbomBlob := []byte(`{"spdxVersion":"SPDX-2.3"}`)
|
||||||
|
sbomFile, err := os.CreateTemp("", "zot-trivy-sbom-test-*.json")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
_, err = sbomFile.Write(sbomBlob)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
err = sbomFile.Close()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
defer os.Remove(sbomFile.Name())
|
||||||
|
|
||||||
|
sbom := &generatedSBOM{
|
||||||
|
filePath: sbomFile.Name(),
|
||||||
|
digest: godigest.FromBytes(sbomBlob),
|
||||||
|
size: int64(len(sbomBlob)),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = scanner.storeSBOMAsOCIArtifact(ctx, "repo", subjectDigest.String(), sbom)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
referrers, err := store.GetReferrers("repo", subjectDigest, []string{defaultSBOMArtifactType})
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(len(referrers.Manifests), ShouldEqual, 1)
|
||||||
|
So(referrers.Manifests[0].ArtifactType, ShouldEqual, defaultSBOMArtifactType)
|
||||||
|
|
||||||
|
metaReferrers, err := metaDB.GetReferrersInfo("repo", subjectDigest, []string{defaultSBOMArtifactType})
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(len(metaReferrers), ShouldEqual, 1)
|
||||||
|
So(metaReferrers[0].Digest, ShouldEqual, referrers.Manifests[0].Digest.String())
|
||||||
|
|
||||||
|
refManifestBlob, _, _, err := store.GetImageManifest("repo", referrers.Manifests[0].Digest.String())
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
var refManifest ispec.Manifest
|
||||||
|
err = json.Unmarshal(refManifestBlob, &refManifest)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(refManifest.Subject.Digest, ShouldEqual, subjectDigest)
|
||||||
|
So(refManifest.Layers[0].MediaType, ShouldEqual, defaultSBOMLayerMediaType)
|
||||||
|
|
||||||
|
err = scanner.storeSBOMAsOCIArtifact(ctx, "repo", subjectDigest.String(), sbom)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
referrers, err = store.GetReferrers("repo", subjectDigest, []string{defaultSBOMArtifactType})
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(len(referrers.Manifests), ShouldEqual, 1)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetCVEReference(t *testing.T) {
|
func TestGetCVEReference(t *testing.T) {
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ PATH=$PATH:${SCRIPTPATH}/../../hack/tools/bin
|
|||||||
echo "Setting up Docker images..."
|
echo "Setting up Docker images..."
|
||||||
${SCRIPTPATH}/setup_images.sh
|
${SCRIPTPATH}/setup_images.sh
|
||||||
|
|
||||||
tests=("pushpull" "pushpull_authn" "delete_images" "referrers" "metadata" "anonymous_policy"
|
tests=("pushpull" "pushpull_authn" "delete_images" "referrers" "sbom" "metadata" "anonymous_policy"
|
||||||
"annotations" "detect_manifest_collision" "cve" "sync" "sync_docker" "sync_replica_cluster"
|
"annotations" "detect_manifest_collision" "cve" "sync" "sync_docker" "sync_replica_cluster"
|
||||||
"scrub" "garbage_collect" "metrics" "metrics_minimal" "multiarch_index" "docker_compat" "redis_local" "redis_session_store"
|
"scrub" "garbage_collect" "metrics" "metrics_minimal" "multiarch_index" "docker_compat" "redis_local" "redis_session_store"
|
||||||
"events_nats" "events_http" "events_nats_lint_failure" "events_http_lint_failure" "events_sink_failure" "events_config_decoding"
|
"events_nats" "events_http" "events_nats_lint_failure" "events_http_lint_failure" "events_sink_failure" "events_config_decoding"
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# Note: Intended to be run as "make run-blackbox-tests" or "make run-blackbox-ci"
|
||||||
|
# Makefile target installs & checks all necessary tooling.
|
||||||
|
|
||||||
|
load helpers_zot
|
||||||
|
load ../port_helper
|
||||||
|
|
||||||
|
function verify_prerequisites {
|
||||||
|
if [ ! $(command -v curl) ]; then
|
||||||
|
echo "you need to install curl as a prerequisite to running the tests" >&3
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! $(command -v jq) ]; then
|
||||||
|
echo "you need to install jq as a prerequisite to running the tests" >&3
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! $(command -v skopeo) ]; then
|
||||||
|
echo "you need to install skopeo as a prerequisite to running the tests" >&3
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup_file() {
|
||||||
|
if ! $(verify_prerequisites); then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
skopeo --insecure-policy copy --format=oci \
|
||||||
|
docker://ghcr.io/project-zot/golang:1.20 \
|
||||||
|
oci:${TEST_DATA_DIR}/golang:1.20
|
||||||
|
|
||||||
|
local zot_root_dir=${BATS_FILE_TMPDIR}/zot
|
||||||
|
local zot_config_file=${BATS_FILE_TMPDIR}/zot_config.json
|
||||||
|
mkdir -p ${zot_root_dir}
|
||||||
|
|
||||||
|
zot_port=$(get_free_port_for_service "zot")
|
||||||
|
echo ${zot_port} > ${BATS_FILE_TMPDIR}/zot.port
|
||||||
|
|
||||||
|
cat >${zot_config_file} <<EOF
|
||||||
|
{
|
||||||
|
"distSpecVersion": "1.1.1",
|
||||||
|
"storage": {
|
||||||
|
"rootDirectory": "${zot_root_dir}"
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"address": "0.0.0.0",
|
||||||
|
"port": "${zot_port}"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"level": "debug",
|
||||||
|
"output": "${BATS_FILE_TMPDIR}/zot.log"
|
||||||
|
},
|
||||||
|
"extensions": {
|
||||||
|
"search": {
|
||||||
|
"enable": true,
|
||||||
|
"cve": {
|
||||||
|
"updateInterval": "24h",
|
||||||
|
"trivy": {
|
||||||
|
"sbom": {
|
||||||
|
"enable": true,
|
||||||
|
"format": "spdx-json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
zot_serve ${ZOT_PATH} ${zot_config_file}
|
||||||
|
wait_zot_reachable ${zot_port}
|
||||||
|
}
|
||||||
|
|
||||||
|
function teardown() {
|
||||||
|
cat ${BATS_FILE_TMPDIR}/zot.log
|
||||||
|
}
|
||||||
|
|
||||||
|
function teardown_file() {
|
||||||
|
zot_stop_all
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "sbom referrer is created and queryable via REST+GraphQL" {
|
||||||
|
zot_port=`cat ${BATS_FILE_TMPDIR}/zot.port`
|
||||||
|
|
||||||
|
run skopeo --insecure-policy copy --dest-tls-verify=false \
|
||||||
|
oci:${TEST_DATA_DIR}/golang:1.20 \
|
||||||
|
docker://127.0.0.1:${zot_port}/golang:1.20
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
|
||||||
|
run skopeo inspect --tls-verify=false docker://127.0.0.1:${zot_port}/golang:1.20
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
manifest_digest=$(echo "${output}" | jq -r '.Digest')
|
||||||
|
[ -n "${manifest_digest}" ]
|
||||||
|
|
||||||
|
cve_query="{ \"query\": \"{ CVEListForImage(image: \\\"golang@${manifest_digest}\\\", requestedPage: {limit: 1, offset: 0}) { Tag Page { TotalCount ItemCount } Summary { Count } } }\" }"
|
||||||
|
run curl -sS -X POST -H "Content-Type: application/json" --data "${cve_query}" \
|
||||||
|
http://127.0.0.1:${zot_port}/v2/_zot/ext/search
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ $(echo "${output}" | jq -r '.data.CVEListForImage.Tag != null') = "true" ]
|
||||||
|
|
||||||
|
sbom_ref_digest=""
|
||||||
|
for _ in $(seq 1 40); do
|
||||||
|
resp=$(curl -sS "http://127.0.0.1:${zot_port}/v2/golang/referrers/${manifest_digest}?artifactType=application/spdx%2Bjson")
|
||||||
|
sbom_ref_digest=$(echo "${resp}" | jq -r '.manifests[0].digest // ""')
|
||||||
|
if [ -n "${sbom_ref_digest}" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "${sbom_ref_digest}" ]
|
||||||
|
[ $(echo "${resp}" | jq -r '.manifests[0].artifactType') = "application/spdx+json" ]
|
||||||
|
|
||||||
|
ref_query="{ \"query\": \"{ Referrers(repo:\\\"golang\\\", digest:\\\"${manifest_digest}\\\", type:[\\\"application/spdx+json\\\"]) { MediaType ArtifactType Digest Size } }\" }"
|
||||||
|
run curl -sS -X POST -H "Content-Type: application/json" --data "${ref_query}" \
|
||||||
|
http://127.0.0.1:${zot_port}/v2/_zot/ext/search
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ $(echo "${output}" | jq -r '.data.Referrers[0].ArtifactType') = "application/spdx+json" ]
|
||||||
|
[ $(echo "${output}" | jq -r '.data.Referrers[0].Digest') = "${sbom_ref_digest}" ]
|
||||||
|
|
||||||
|
run curl -sS -H "Accept: application/vnd.oci.image.manifest.v1+json" \
|
||||||
|
http://127.0.0.1:${zot_port}/v2/golang/manifests/${sbom_ref_digest}
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
sbom_layer_digest=$(echo "${output}" | jq -r '.layers[0].digest')
|
||||||
|
[ -n "${sbom_layer_digest}" ]
|
||||||
|
|
||||||
|
run curl -sS http://127.0.0.1:${zot_port}/v2/golang/blobs/${sbom_layer_digest}
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ $(echo "${output}" | jq -r '.spdxVersion') = "SPDX-2.3" ]
|
||||||
|
[ $(echo "${output}" | jq -r '.packages | length') -gt 0 ]
|
||||||
|
}
|
||||||
@@ -169,6 +169,12 @@
|
|||||||
"end": 1909
|
"end": 1909
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"blackbox/sbom.bats": {
|
||||||
|
"zot": {
|
||||||
|
"begin": 1910,
|
||||||
|
"end": 1919
|
||||||
|
}
|
||||||
|
},
|
||||||
"blackbox/restore_s3_blobs.bats": {
|
"blackbox/restore_s3_blobs.bats": {
|
||||||
"zot": {
|
"zot": {
|
||||||
"begin": 1920,
|
"begin": 1920,
|
||||||
|
|||||||
Reference in New Issue
Block a user