mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-23 07:29:00 -08:00
feat(image): prevent scanning oversized container images (#8178)
Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io> Signed-off-by: knqyf263 <knqyf263@gmail.com> Co-authored-by: knqyf263 <knqyf263@gmail.com>
This commit is contained in:
@@ -21,6 +21,12 @@ func main() {
|
|||||||
if errors.As(err, &exitError) {
|
if errors.As(err, &exitError) {
|
||||||
os.Exit(exitError.Code)
|
os.Exit(exitError.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userErr *types.UserError
|
||||||
|
if errors.As(err, &userErr) {
|
||||||
|
log.Fatal("Error", log.Err(userErr))
|
||||||
|
}
|
||||||
|
|
||||||
log.Fatal("Fatal error", log.Err(err))
|
log.Fatal("Fatal error", log.Err(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ trivy image [flags] IMAGE_NAME
|
|||||||
--license-confidence-level float specify license classifier's confidence level (default 0.9)
|
--license-confidence-level float specify license classifier's confidence level (default 0.9)
|
||||||
--license-full eagerly look for licenses in source code headers and license files
|
--license-full eagerly look for licenses in source code headers and license files
|
||||||
--list-all-pkgs output all packages in the JSON report regardless of vulnerability
|
--list-all-pkgs output all packages in the JSON report regardless of vulnerability
|
||||||
|
--max-image-size string [EXPERIMENTAL] maximum image size to process, specified in a human-readable format (e.g., '44kB', '17MB'); an error will be returned if the image exceeds this size
|
||||||
--misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan-json,terraformplan-snapshot])
|
--misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan-json,terraformplan-snapshot])
|
||||||
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
|
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
|
||||||
--no-progress suppress progress bar
|
--no-progress suppress progress bar
|
||||||
|
|||||||
@@ -137,6 +137,9 @@ image:
|
|||||||
# Same as '--input'
|
# Same as '--input'
|
||||||
input: ""
|
input: ""
|
||||||
|
|
||||||
|
# Same as '--max-image-size'
|
||||||
|
max-size: ""
|
||||||
|
|
||||||
# Same as '--platform'
|
# Same as '--platform'
|
||||||
platform: ""
|
platform: ""
|
||||||
|
|
||||||
|
|||||||
@@ -518,3 +518,21 @@ You can configure Podman daemon socket with `--podman-host`.
|
|||||||
```shell
|
```shell
|
||||||
$ trivy image --podman-host /run/user/1000/podman/podman.sock YOUR_IMAGE
|
$ trivy image --podman-host /run/user/1000/podman/podman.sock YOUR_IMAGE
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Prevent scanning oversized container images
|
||||||
|
Use the `--max-image-size` flag to avoid scanning images that exceed a specified size. The size is specified in a human-readable format (e.g., `100MB`, `10GB`). If the compressed image size exceeds the specified threshold, an error is returned immediately. Otherwise, all layers are pulled, stored in a temporary folder, and their uncompressed size is verified before scanning. Temporary layers are always cleaned up, even after a successful scan.
|
||||||
|
|
||||||
|
!!! warning "EXPERIMENTAL"
|
||||||
|
This feature might change without preserving backwards compatibility.
|
||||||
|
|
||||||
|
|
||||||
|
Example Usage:
|
||||||
|
```bash
|
||||||
|
# Limit uncompressed image size to 10GB
|
||||||
|
$ trivy image --max-image-size=10GB myapp:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Error Output:
|
||||||
|
```bash
|
||||||
|
Error: uncompressed image size (15GB) exceeds maximum allowed size (10GB)
|
||||||
|
```
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -46,6 +46,7 @@ require (
|
|||||||
github.com/docker/cli v27.5.0+incompatible
|
github.com/docker/cli v27.5.0+incompatible
|
||||||
github.com/docker/docker v27.5.0+incompatible
|
github.com/docker/docker v27.5.0+incompatible
|
||||||
github.com/docker/go-connections v0.5.0
|
github.com/docker/go-connections v0.5.0
|
||||||
|
github.com/docker/go-units v0.5.0
|
||||||
github.com/fatih/color v1.18.0
|
github.com/fatih/color v1.18.0
|
||||||
github.com/go-git/go-git/v5 v5.13.1
|
github.com/go-git/go-git/v5 v5.13.1
|
||||||
github.com/go-openapi/runtime v0.28.0 // indirect
|
github.com/go-openapi/runtime v0.28.0 // indirect
|
||||||
@@ -222,7 +223,6 @@ require (
|
|||||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||||
github.com/docker/go-metrics v0.0.1 // indirect
|
github.com/docker/go-metrics v0.0.1 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
|
||||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
|
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
|
||||||
github.com/dsnet/compress v0.0.1 // indirect
|
github.com/dsnet/compress v0.0.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ func TestDockerEngine(t *testing.T) {
|
|||||||
ignoreStatus []string
|
ignoreStatus []string
|
||||||
severity []string
|
severity []string
|
||||||
ignoreIDs []string
|
ignoreIDs []string
|
||||||
|
maxImageSize string
|
||||||
input string
|
input string
|
||||||
golden string
|
golden string
|
||||||
wantErr string
|
wantErr string
|
||||||
@@ -34,6 +35,12 @@ func TestDockerEngine(t *testing.T) {
|
|||||||
input: "testdata/fixtures/images/alpine-39.tar.gz",
|
input: "testdata/fixtures/images/alpine-39.tar.gz",
|
||||||
golden: "testdata/alpine-39.json.golden",
|
golden: "testdata/alpine-39.json.golden",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "alpine:3.9, with max image size",
|
||||||
|
maxImageSize: "100mb",
|
||||||
|
input: "testdata/fixtures/images/alpine-39.tar.gz",
|
||||||
|
golden: "testdata/alpine-39.json.golden",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "alpine:3.9, with high and critical severity",
|
name: "alpine:3.9, with high and critical severity",
|
||||||
severity: []string{
|
severity: []string{
|
||||||
@@ -195,6 +202,12 @@ func TestDockerEngine(t *testing.T) {
|
|||||||
input: "badimage:latest",
|
input: "badimage:latest",
|
||||||
wantErr: "unable to inspect the image (badimage:latest)",
|
wantErr: "unable to inspect the image (badimage:latest)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "sad path, image size is larger than the maximum",
|
||||||
|
input: "testdata/fixtures/images/alpine-39.tar.gz",
|
||||||
|
maxImageSize: "3mb",
|
||||||
|
wantErr: "uncompressed image size 5.8MB exceeds maximum allowed size 3MB",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up testing DB
|
// Set up testing DB
|
||||||
@@ -263,6 +276,11 @@ func TestDockerEngine(t *testing.T) {
|
|||||||
require.NoError(t, err, "failed to write .trivyignore")
|
require.NoError(t, err, "failed to write .trivyignore")
|
||||||
defer os.Remove(trivyIgnore)
|
defer os.Remove(trivyIgnore)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tt.maxImageSize != "" {
|
||||||
|
osArgs = append(osArgs, []string{"--max-image-size", tt.maxImageSize}...)
|
||||||
|
}
|
||||||
|
|
||||||
osArgs = append(osArgs, tt.input)
|
osArgs = append(osArgs, tt.input)
|
||||||
|
|
||||||
// Run Trivy
|
// Run Trivy
|
||||||
|
|||||||
@@ -587,6 +587,7 @@ func (r *runner) initScannerConfig(ctx context.Context, opts flag.Options) (Scan
|
|||||||
Host: opts.PodmanHost,
|
Host: opts.PodmanHost,
|
||||||
},
|
},
|
||||||
ImageSources: opts.ImageSources,
|
ImageSources: opts.ImageSources,
|
||||||
|
MaxImageSize: opts.MaxImageSize,
|
||||||
},
|
},
|
||||||
|
|
||||||
// For misconfiguration scanning
|
// For misconfiguration scanning
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ package image
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/docker/go-units"
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
@@ -24,6 +27,7 @@ import (
|
|||||||
"github.com/aquasecurity/trivy/pkg/log"
|
"github.com/aquasecurity/trivy/pkg/log"
|
||||||
"github.com/aquasecurity/trivy/pkg/parallel"
|
"github.com/aquasecurity/trivy/pkg/parallel"
|
||||||
"github.com/aquasecurity/trivy/pkg/semaphore"
|
"github.com/aquasecurity/trivy/pkg/semaphore"
|
||||||
|
trivyTypes "github.com/aquasecurity/trivy/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Artifact struct {
|
type Artifact struct {
|
||||||
@@ -36,6 +40,8 @@ type Artifact struct {
|
|||||||
handlerManager handler.Manager
|
handlerManager handler.Manager
|
||||||
|
|
||||||
artifactOption artifact.Option
|
artifactOption artifact.Option
|
||||||
|
|
||||||
|
layerCacheDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
type LayerInfo struct {
|
type LayerInfo struct {
|
||||||
@@ -60,6 +66,11 @@ func NewArtifact(img types.Image, c cache.ArtifactCache, opt artifact.Option) (a
|
|||||||
return nil, xerrors.Errorf("config analyzer group error: %w", err)
|
return nil, xerrors.Errorf("config analyzer group error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cacheDir, err := os.MkdirTemp("", "layers")
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("failed to create a cache layers temp dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return Artifact{
|
return Artifact{
|
||||||
logger: log.WithPrefix("image"),
|
logger: log.WithPrefix("image"),
|
||||||
image: img,
|
image: img,
|
||||||
@@ -70,10 +81,11 @@ func NewArtifact(img types.Image, c cache.ArtifactCache, opt artifact.Option) (a
|
|||||||
handlerManager: handlerManager,
|
handlerManager: handlerManager,
|
||||||
|
|
||||||
artifactOption: opt,
|
artifactOption: opt,
|
||||||
|
layerCacheDir: cacheDir,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Artifact) Inspect(ctx context.Context) (artifact.Reference, error) {
|
func (a Artifact) Inspect(ctx context.Context) (ref artifact.Reference, err error) {
|
||||||
imageID, err := a.image.ID()
|
imageID, err := a.image.ID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return artifact.Reference{}, xerrors.Errorf("unable to get the image ID: %w", err)
|
return artifact.Reference{}, xerrors.Errorf("unable to get the image ID: %w", err)
|
||||||
@@ -88,6 +100,15 @@ func (a Artifact) Inspect(ctx context.Context) (artifact.Reference, error) {
|
|||||||
diffIDs := a.diffIDs(configFile)
|
diffIDs := a.diffIDs(configFile)
|
||||||
a.logger.Debug("Detected diff ID", log.Any("diff_ids", diffIDs))
|
a.logger.Debug("Detected diff ID", log.Any("diff_ids", diffIDs))
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if rerr := os.RemoveAll(a.layerCacheDir); rerr != nil {
|
||||||
|
log.Error("Failed to remove layer cache", log.Err(rerr))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := a.checkImageSize(ctx, diffIDs); err != nil {
|
||||||
|
return artifact.Reference{}, err
|
||||||
|
}
|
||||||
|
|
||||||
// Try retrieving a remote SBOM document
|
// Try retrieving a remote SBOM document
|
||||||
if res, err := a.retrieveRemoteSBOM(ctx); err == nil {
|
if res, err := a.retrieveRemoteSBOM(ctx); err == nil {
|
||||||
// Found SBOM
|
// Found SBOM
|
||||||
@@ -141,7 +162,7 @@ func (a Artifact) Inspect(ctx context.Context) (artifact.Reference, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Artifact) Clean(_ artifact.Reference) error {
|
func (a Artifact) Clean(_ artifact.Reference) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +219,107 @@ func (a Artifact) consolidateCreatedBy(diffIDs, layerKeys []string, configFile *
|
|||||||
return layerKeyMap
|
return layerKeyMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func limitErrorMessage(typ string, maxSize, imageSize int64) string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s image size %s exceeds maximum allowed size %s", typ,
|
||||||
|
units.HumanSizeWithPrecision(float64(imageSize), 3),
|
||||||
|
units.HumanSize(float64(maxSize)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Artifact) checkImageSize(ctx context.Context, diffIDs []string) error {
|
||||||
|
maxSize := a.artifactOption.ImageOption.MaxImageSize
|
||||||
|
if maxSize == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
compressedSize, err := a.compressedImageSize(diffIDs)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("failed to get compressed image size: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if compressedSize > maxSize {
|
||||||
|
return &trivyTypes.UserError{
|
||||||
|
Message: limitErrorMessage("compressed", maxSize, compressedSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imageSize, err := a.imageSize(ctx, diffIDs)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("failed to calculate image size: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if imageSize > maxSize {
|
||||||
|
return &trivyTypes.UserError{
|
||||||
|
Message: limitErrorMessage("uncompressed", maxSize, imageSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Artifact) compressedImageSize(diffIDs []string) (int64, error) {
|
||||||
|
var totalSize int64
|
||||||
|
for _, diffID := range diffIDs {
|
||||||
|
h, err := v1.NewHash(diffID)
|
||||||
|
if err != nil {
|
||||||
|
return -1, xerrors.Errorf("invalid layer ID (%s): %w", diffID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer, err := a.image.LayerByDiffID(h)
|
||||||
|
if err != nil {
|
||||||
|
return -1, xerrors.Errorf("failed to get the layer (%s): %w", diffID, err)
|
||||||
|
}
|
||||||
|
layerSize, err := layer.Size()
|
||||||
|
if err != nil {
|
||||||
|
return -1, xerrors.Errorf("failed to get layer size: %w", err)
|
||||||
|
}
|
||||||
|
totalSize += layerSize
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalSize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Artifact) imageSize(ctx context.Context, diffIDs []string) (int64, error) {
|
||||||
|
var imageSize int64
|
||||||
|
|
||||||
|
p := parallel.NewPipeline(a.artifactOption.Parallel, false, diffIDs,
|
||||||
|
func(_ context.Context, diffID string) (int64, error) {
|
||||||
|
layerSize, err := a.saveLayer(diffID)
|
||||||
|
if err != nil {
|
||||||
|
return -1, xerrors.Errorf("failed to save layer: %w", err)
|
||||||
|
}
|
||||||
|
return layerSize, nil
|
||||||
|
},
|
||||||
|
func(layerSize int64) error {
|
||||||
|
imageSize += layerSize
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := p.Do(ctx); err != nil {
|
||||||
|
return -1, xerrors.Errorf("pipeline error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageSize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Artifact) saveLayer(diffID string) (int64, error) {
|
||||||
|
a.logger.Debug("Pulling the layer to the local cache", log.String("diff_id", diffID))
|
||||||
|
_, rc, err := a.uncompressedLayer(diffID)
|
||||||
|
if err != nil {
|
||||||
|
return -1, xerrors.Errorf("unable to get uncompressed layer %s: %w", diffID, err)
|
||||||
|
}
|
||||||
|
defer rc.Close()
|
||||||
|
|
||||||
|
f, err := os.Create(filepath.Join(a.layerCacheDir, diffID))
|
||||||
|
if err != nil {
|
||||||
|
return -1, xerrors.Errorf("failed to create a file: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
return io.Copy(f, rc)
|
||||||
|
}
|
||||||
|
|
||||||
func (a Artifact) inspect(ctx context.Context, missingImage string, layerKeys, baseDiffIDs []string,
|
func (a Artifact) inspect(ctx context.Context, missingImage string, layerKeys, baseDiffIDs []string,
|
||||||
layerKeyMap map[string]LayerInfo, configFile *v1.ConfigFile) error {
|
layerKeyMap map[string]LayerInfo, configFile *v1.ConfigFile) error {
|
||||||
|
|
||||||
@@ -361,6 +483,12 @@ func (a Artifact) uncompressedLayer(diffID string) (string, io.ReadCloser, error
|
|||||||
digest = d.String()
|
digest = d.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(filepath.Join(a.layerCacheDir, diffID))
|
||||||
|
if err == nil {
|
||||||
|
a.logger.Debug("Loaded the layer from the local cache", log.String("diff_id", diffID))
|
||||||
|
return digest, f, nil
|
||||||
|
}
|
||||||
|
|
||||||
rc, err := layer.Uncompressed()
|
rc, err := layer.Uncompressed()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, xerrors.Errorf("failed to get the layer content (%s): %w", diffID, err)
|
return "", nil, xerrors.Errorf("failed to get the layer content (%s): %w", diffID, err)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/go-units"
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -348,6 +349,7 @@ func TestArtifact_Inspect(t *testing.T) {
|
|||||||
imagePath: "../../test/testdata/alpine-311.tar.gz",
|
imagePath: "../../test/testdata/alpine-311.tar.gz",
|
||||||
artifactOpt: artifact.Option{
|
artifactOpt: artifact.Option{
|
||||||
LicenseScannerOption: analyzer.LicenseScannerOption{Full: true},
|
LicenseScannerOption: analyzer.LicenseScannerOption{Full: true},
|
||||||
|
ImageOption: types.ImageOptions{MaxImageSize: units.GB},
|
||||||
},
|
},
|
||||||
missingBlobsExpectation: cache.ArtifactCacheMissingBlobsExpectation{
|
missingBlobsExpectation: cache.ArtifactCacheMissingBlobsExpectation{
|
||||||
Args: cache.ArtifactCacheMissingBlobsArgs{
|
Args: cache.ArtifactCacheMissingBlobsArgs{
|
||||||
@@ -2243,6 +2245,22 @@ func TestArtifact_Inspect(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: "put artifact failed",
|
wantErr: "put artifact failed",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "sad path, compressed image size is larger than the maximum",
|
||||||
|
imagePath: "../../test/testdata/alpine-311.tar.gz",
|
||||||
|
artifactOpt: artifact.Option{
|
||||||
|
ImageOption: types.ImageOptions{MaxImageSize: units.MB * 1},
|
||||||
|
},
|
||||||
|
wantErr: "compressed image size 3.03MB exceeds maximum allowed size 1MB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sad path, image size is larger than the maximum",
|
||||||
|
imagePath: "../../test/testdata/alpine-311.tar.gz",
|
||||||
|
artifactOpt: artifact.Option{
|
||||||
|
ImageOption: types.ImageOptions{MaxImageSize: units.MB * 4},
|
||||||
|
},
|
||||||
|
wantErr: "uncompressed image size 5.86MB exceeds maximum allowed size 4MB",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@@ -2262,6 +2280,8 @@ func TestArtifact_Inspect(t *testing.T) {
|
|||||||
assert.ErrorContains(t, err, tt.wantErr, tt.name)
|
assert.ErrorContains(t, err, tt.wantErr, tt.name)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer a.Clean(got)
|
||||||
|
|
||||||
require.NoError(t, err, tt.name)
|
require.NoError(t, err, tt.name)
|
||||||
assert.Equal(t, tt.want, got)
|
assert.Equal(t, tt.want, got)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -170,6 +170,8 @@ func TestArtifact_InspectRekorAttestation(t *testing.T) {
|
|||||||
assert.ErrorContains(t, err, tt.wantErr)
|
assert.ErrorContains(t, err, tt.wantErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer a.Clean(got)
|
||||||
|
|
||||||
require.NoError(t, err, tt.name)
|
require.NoError(t, err, tt.name)
|
||||||
got.BOM = nil
|
got.BOM = nil
|
||||||
assert.Equal(t, tt.want, got)
|
assert.Equal(t, tt.want, got)
|
||||||
@@ -312,6 +314,7 @@ func TestArtifact_inspectOCIReferrerSBOM(t *testing.T) {
|
|||||||
assert.ErrorContains(t, err, tt.wantErr)
|
assert.ErrorContains(t, err, tt.wantErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer a.Clean(got)
|
||||||
|
|
||||||
require.NoError(t, err, tt.name)
|
require.NoError(t, err, tt.name)
|
||||||
got.BOM = nil
|
got.BOM = nil
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ func analyze(ctx context.Context, imageRef string, opt types.ImageOptions) (*typ
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer ar.Clean(imageInfo)
|
||||||
|
|
||||||
imageDetail, err := ap.ApplyLayers(imageInfo.ID, imageInfo.BlobIDs)
|
imageDetail, err := ap.ApplyLayers(imageInfo.ID, imageInfo.BlobIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ type ImageOptions struct {
|
|||||||
PodmanOptions PodmanOptions
|
PodmanOptions PodmanOptions
|
||||||
ContainerdOptions ContainerdOptions
|
ContainerdOptions ContainerdOptions
|
||||||
ImageSources ImageSources
|
ImageSources ImageSources
|
||||||
|
MaxImageSize int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type DockerOptions struct {
|
type DockerOptions struct {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package flag
|
package flag
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/docker/go-units"
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
@@ -58,6 +59,12 @@ var (
|
|||||||
Values: xstrings.ToStringSlice(ftypes.AllImageSources),
|
Values: xstrings.ToStringSlice(ftypes.AllImageSources),
|
||||||
Usage: "image source(s) to use, in priority order",
|
Usage: "image source(s) to use, in priority order",
|
||||||
}
|
}
|
||||||
|
MaxImageSize = Flag[string]{
|
||||||
|
Name: "max-image-size",
|
||||||
|
ConfigName: "image.max-size",
|
||||||
|
Default: "",
|
||||||
|
Usage: "[EXPERIMENTAL] maximum image size to process, specified in a human-readable format (e.g., '44kB', '17MB'); an error will be returned if the image exceeds this size",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type ImageFlagGroup struct {
|
type ImageFlagGroup struct {
|
||||||
@@ -68,6 +75,7 @@ type ImageFlagGroup struct {
|
|||||||
DockerHost *Flag[string]
|
DockerHost *Flag[string]
|
||||||
PodmanHost *Flag[string]
|
PodmanHost *Flag[string]
|
||||||
ImageSources *Flag[[]string]
|
ImageSources *Flag[[]string]
|
||||||
|
MaxImageSize *Flag[string]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageOptions struct {
|
type ImageOptions struct {
|
||||||
@@ -78,6 +86,7 @@ type ImageOptions struct {
|
|||||||
DockerHost string
|
DockerHost string
|
||||||
PodmanHost string
|
PodmanHost string
|
||||||
ImageSources ftypes.ImageSources
|
ImageSources ftypes.ImageSources
|
||||||
|
MaxImageSize int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewImageFlagGroup() *ImageFlagGroup {
|
func NewImageFlagGroup() *ImageFlagGroup {
|
||||||
@@ -89,6 +98,7 @@ func NewImageFlagGroup() *ImageFlagGroup {
|
|||||||
DockerHost: DockerHostFlag.Clone(),
|
DockerHost: DockerHostFlag.Clone(),
|
||||||
PodmanHost: PodmanHostFlag.Clone(),
|
PodmanHost: PodmanHostFlag.Clone(),
|
||||||
ImageSources: SourceFlag.Clone(),
|
ImageSources: SourceFlag.Clone(),
|
||||||
|
MaxImageSize: MaxImageSize.Clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +115,7 @@ func (f *ImageFlagGroup) Flags() []Flagger {
|
|||||||
f.DockerHost,
|
f.DockerHost,
|
||||||
f.PodmanHost,
|
f.PodmanHost,
|
||||||
f.ImageSources,
|
f.ImageSources,
|
||||||
|
f.MaxImageSize,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +135,14 @@ func (f *ImageFlagGroup) ToOptions() (ImageOptions, error) {
|
|||||||
}
|
}
|
||||||
platform = ftypes.Platform{Platform: pl}
|
platform = ftypes.Platform{Platform: pl}
|
||||||
}
|
}
|
||||||
|
var maxSize int64
|
||||||
|
if value := f.MaxImageSize.Value(); value != "" {
|
||||||
|
parsedSize, err := units.FromHumanSize(value)
|
||||||
|
if err != nil {
|
||||||
|
return ImageOptions{}, xerrors.Errorf("invalid max image size %q: %w", value, err)
|
||||||
|
}
|
||||||
|
maxSize = parsedSize
|
||||||
|
}
|
||||||
|
|
||||||
return ImageOptions{
|
return ImageOptions{
|
||||||
Input: f.Input.Value(),
|
Input: f.Input.Value(),
|
||||||
@@ -133,5 +152,6 @@ func (f *ImageFlagGroup) ToOptions() (ImageOptions, error) {
|
|||||||
DockerHost: f.DockerHost.Value(),
|
DockerHost: f.DockerHost.Value(),
|
||||||
PodmanHost: f.PodmanHost.Value(),
|
PodmanHost: f.PodmanHost.Value(),
|
||||||
ImageSources: xstrings.ToTSlice[ftypes.ImageSource](f.ImageSources.Value()),
|
ImageSources: xstrings.ToTSlice[ftypes.ImageSource](f.ImageSources.Value()),
|
||||||
|
MaxImageSize: maxSize,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
91
pkg/flag/image_flags_test.go
Normal file
91
pkg/flag/image_flags_test.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package flag_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/flag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestImageFlagGroup_ToOptions(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
maxImgSize string
|
||||||
|
platform string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want flag.ImageOptions
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy default (without flags)",
|
||||||
|
fields: fields{},
|
||||||
|
want: flag.ImageOptions{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "happy path with max image size",
|
||||||
|
fields: fields{
|
||||||
|
maxImgSize: "10mb",
|
||||||
|
},
|
||||||
|
want: flag.ImageOptions{
|
||||||
|
MaxImageSize: units.MB * 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid max image size",
|
||||||
|
fields: fields{
|
||||||
|
maxImgSize: "10foo",
|
||||||
|
},
|
||||||
|
wantErr: "invalid max image size",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "happy path with platform",
|
||||||
|
fields: fields{
|
||||||
|
platform: "linux/amd64",
|
||||||
|
},
|
||||||
|
want: flag.ImageOptions{
|
||||||
|
Platform: types.Platform{
|
||||||
|
Platform: &v1.Platform{
|
||||||
|
OS: "linux",
|
||||||
|
Architecture: "amd64",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid platform",
|
||||||
|
fields: fields{
|
||||||
|
platform: "unknown/unknown/unknown/unknown",
|
||||||
|
},
|
||||||
|
wantErr: "unable to parse platform",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Cleanup(viper.Reset)
|
||||||
|
|
||||||
|
setValue(flag.MaxImageSize.ConfigName, tt.fields.maxImgSize)
|
||||||
|
setValue(flag.PlatformFlag.ConfigName, tt.fields.platform)
|
||||||
|
|
||||||
|
f := &flag.ImageFlagGroup{
|
||||||
|
MaxImageSize: flag.MaxImageSize.Clone(),
|
||||||
|
Platform: flag.PlatformFlag.Clone(),
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := f.ToOptions()
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
assert.ErrorContains(t, err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.EqualExportedValues(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,3 +11,12 @@ type ExitError struct {
|
|||||||
func (e *ExitError) Error() string {
|
func (e *ExitError) Error() string {
|
||||||
return fmt.Sprintf("exit status %d", e.Code)
|
return fmt.Sprintf("exit status %d", e.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserError represents an error with a user-friendly message.
|
||||||
|
type UserError struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserError) Error() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user