Skip to content

Commit 73bd20d

Browse files
nikpivkinknqyf263
andauthored
feat(image): return error early if total size of layers exceeds limit (#8294)
Signed-off-by: nikpivkin <[email protected]> Signed-off-by: knqyf263 <[email protected]> Co-authored-by: knqyf263 <[email protected]>
1 parent 0031a38 commit 73bd20d

File tree

5 files changed

+84
-55
lines changed

5 files changed

+84
-55
lines changed

docs/docs/target/container_image.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,14 @@ $ trivy image --podman-host /run/user/1000/podman/podman.sock YOUR_IMAGE
526526
```
527527

528528
### Prevent scanning oversized container images
529-
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.
529+
Use the `--max-image-size` flag to avoid scanning images that exceed a specified size. The size is specified in a human-readable format[^1] (e.g., `100MB`, `10GB`).
530+
531+
An error is returned in the following cases:
532+
533+
- if the compressed image size exceeds the limit,
534+
- if the accumulated size of the uncompressed layers exceeds the limit during their pulling.
535+
536+
The layers are pulled into a temporary folder during their pulling and are always cleaned up, even after a successful scan.
530537

531538
!!! warning "EXPERIMENTAL"
532539
This feature might change without preserving backwards compatibility.
@@ -542,3 +549,5 @@ Error Output:
542549
```bash
543550
Error: uncompressed image size (15GB) exceeds maximum allowed size (10GB)
544551
```
552+
553+
[^1]: Trivy uses decimal (SI) prefixes (based on 1000) for size.

integration/docker_engine_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ func TestDockerEngine(t *testing.T) {
206206
name: "sad path, image size is larger than the maximum",
207207
input: "testdata/fixtures/images/alpine-39.tar.gz",
208208
maxImageSize: "3mb",
209-
wantErr: "uncompressed image size 5.8MB exceeds maximum allowed size 3MB",
209+
wantErr: "uncompressed layers size 5.8MB exceeds maximum allowed size 3MB",
210210
},
211211
}
212212

pkg/fanal/artifact/image/image.go

+29-34
Original file line numberDiff line numberDiff line change
@@ -219,68 +219,60 @@ func (a Artifact) consolidateCreatedBy(diffIDs, layerKeys []string, configFile *
219219
return layerKeyMap
220220
}
221221

222-
func limitErrorMessage(typ string, maxSize, imageSize int64) string {
223-
return fmt.Sprintf(
224-
"%s image size %s exceeds maximum allowed size %s", typ,
225-
units.HumanSizeWithPrecision(float64(imageSize), 3),
226-
units.HumanSize(float64(maxSize)),
227-
)
222+
func (a Artifact) imageSizeError(typ string, size int64) error {
223+
return &trivyTypes.UserError{
224+
Message: fmt.Sprintf(
225+
"%s size %s exceeds maximum allowed size %s", typ,
226+
units.HumanSizeWithPrecision(float64(size), 3),
227+
units.HumanSize(float64(a.artifactOption.ImageOption.MaxImageSize)),
228+
),
229+
}
228230
}
229231

230232
func (a Artifact) checkImageSize(ctx context.Context, diffIDs []string) error {
231-
maxSize := a.artifactOption.ImageOption.MaxImageSize
232-
if maxSize == 0 {
233+
if a.artifactOption.ImageOption.MaxImageSize == 0 {
233234
return nil
234235
}
235236

236-
compressedSize, err := a.compressedImageSize(diffIDs)
237-
if err != nil {
237+
if err := a.checkCompressedImageSize(diffIDs); err != nil {
238238
return xerrors.Errorf("failed to get compressed image size: %w", err)
239239
}
240240

241-
if compressedSize > maxSize {
242-
return &trivyTypes.UserError{
243-
Message: limitErrorMessage("compressed", maxSize, compressedSize),
244-
}
245-
}
246-
247-
imageSize, err := a.imageSize(ctx, diffIDs)
248-
if err != nil {
241+
if err := a.checkUncompressedImageSize(ctx, diffIDs); err != nil {
249242
return xerrors.Errorf("failed to calculate image size: %w", err)
250243
}
251-
252-
if imageSize > maxSize {
253-
return &trivyTypes.UserError{
254-
Message: limitErrorMessage("uncompressed", maxSize, imageSize),
255-
}
256-
}
257244
return nil
258245
}
259246

260-
func (a Artifact) compressedImageSize(diffIDs []string) (int64, error) {
247+
func (a Artifact) checkCompressedImageSize(diffIDs []string) error {
261248
var totalSize int64
249+
262250
for _, diffID := range diffIDs {
263251
h, err := v1.NewHash(diffID)
264252
if err != nil {
265-
return -1, xerrors.Errorf("invalid layer ID (%s): %w", diffID, err)
253+
return xerrors.Errorf("invalid layer ID (%s): %w", diffID, err)
266254
}
267255

268256
layer, err := a.image.LayerByDiffID(h)
269257
if err != nil {
270-
return -1, xerrors.Errorf("failed to get the layer (%s): %w", diffID, err)
258+
return xerrors.Errorf("failed to get the layer (%s): %w", diffID, err)
271259
}
272260
layerSize, err := layer.Size()
273261
if err != nil {
274-
return -1, xerrors.Errorf("failed to get layer size: %w", err)
262+
return xerrors.Errorf("failed to get layer size: %w", err)
275263
}
276264
totalSize += layerSize
277265
}
278266

279-
return totalSize, nil
267+
if totalSize > a.artifactOption.ImageOption.MaxImageSize {
268+
return a.imageSizeError("compressed image", totalSize)
269+
}
270+
271+
return nil
280272
}
281273

282-
func (a Artifact) imageSize(ctx context.Context, diffIDs []string) (int64, error) {
283-
var imageSize int64
274+
func (a Artifact) checkUncompressedImageSize(ctx context.Context, diffIDs []string) error {
275+
var totalSize int64
284276

285277
p := parallel.NewPipeline(a.artifactOption.Parallel, false, diffIDs,
286278
func(_ context.Context, diffID string) (int64, error) {
@@ -291,16 +283,19 @@ func (a Artifact) imageSize(ctx context.Context, diffIDs []string) (int64, error
291283
return layerSize, nil
292284
},
293285
func(layerSize int64) error {
294-
imageSize += layerSize
286+
totalSize += layerSize
287+
if totalSize > a.artifactOption.ImageOption.MaxImageSize {
288+
return a.imageSizeError("uncompressed layers", totalSize)
289+
}
295290
return nil
296291
},
297292
)
298293

299294
if err := p.Do(ctx); err != nil {
300-
return -1, xerrors.Errorf("pipeline error: %w", err)
295+
return xerrors.Errorf("pipeline error: %w", err)
301296
}
302297

303-
return imageSize, nil
298+
return nil
304299
}
305300

306301
func (a Artifact) saveLayer(diffID string) (int64, error) {

pkg/fanal/artifact/image/image_test.go

+41-16
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package image_test
33
import (
44
"context"
55
"errors"
6+
"math/rand"
67
"testing"
78
"time"
89

910
"github.com/docker/go-units"
1011
v1 "github.com/google/go-containerregistry/pkg/v1"
12+
"github.com/google/go-containerregistry/pkg/v1/random"
1113
"github.com/stretchr/testify/assert"
1214
"github.com/stretchr/testify/require"
1315
"golang.org/x/xerrors"
@@ -2245,22 +2247,6 @@ func TestArtifact_Inspect(t *testing.T) {
22452247
},
22462248
wantErr: "put artifact failed",
22472249
},
2248-
{
2249-
name: "sad path, compressed image size is larger than the maximum",
2250-
imagePath: "../../test/testdata/alpine-311.tar.gz",
2251-
artifactOpt: artifact.Option{
2252-
ImageOption: types.ImageOptions{MaxImageSize: units.MB * 1},
2253-
},
2254-
wantErr: "compressed image size 3.03MB exceeds maximum allowed size 1MB",
2255-
},
2256-
{
2257-
name: "sad path, image size is larger than the maximum",
2258-
imagePath: "../../test/testdata/alpine-311.tar.gz",
2259-
artifactOpt: artifact.Option{
2260-
ImageOption: types.ImageOptions{MaxImageSize: units.MB * 4},
2261-
},
2262-
wantErr: "uncompressed image size 5.86MB exceeds maximum allowed size 4MB",
2263-
},
22642250
}
22652251
for _, tt := range tests {
22662252
t.Run(tt.name, func(t *testing.T) {
@@ -2287,3 +2273,42 @@ func TestArtifact_Inspect(t *testing.T) {
22872273
})
22882274
}
22892275
}
2276+
2277+
func TestArtifact_InspectWithMaxImageSize(t *testing.T) {
2278+
randomImage, err := random.Image(1000, 2, random.WithSource(rand.NewSource(0)))
2279+
require.NoError(t, err)
2280+
2281+
img := &fakeImage{Image: randomImage}
2282+
mockCache := new(cache.MockArtifactCache)
2283+
2284+
tests := []struct {
2285+
name string
2286+
artifactOpt artifact.Option
2287+
wantErr string
2288+
}{
2289+
{
2290+
name: "compressed image size is larger than the maximum",
2291+
artifactOpt: artifact.Option{
2292+
ImageOption: types.ImageOptions{MaxImageSize: units.KB * 1},
2293+
},
2294+
wantErr: "compressed image size 2.44kB exceeds maximum allowed size 1kB",
2295+
},
2296+
{
2297+
name: "uncompressed layers size is larger than the maximum",
2298+
artifactOpt: artifact.Option{
2299+
ImageOption: types.ImageOptions{MaxImageSize: units.KB * 3},
2300+
},
2301+
wantErr: "uncompressed layers size 5.12kB exceeds maximum allowed size 3kB",
2302+
},
2303+
}
2304+
2305+
for _, tt := range tests {
2306+
t.Run(tt.name, func(t *testing.T) {
2307+
artifact, err := image2.NewArtifact(img, mockCache, tt.artifactOpt)
2308+
require.NoError(t, err)
2309+
2310+
_, err = artifact.Inspect(context.Background())
2311+
require.ErrorContains(t, err, tt.wantErr)
2312+
})
2313+
}
2314+
}

pkg/fanal/artifact/image/remote_sbom_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func TestMain(m *testing.M) {
3131
type fakeImage struct {
3232
name string
3333
repoDigests []string
34-
*fakei.FakeImage
34+
v1.Image
3535
types.ImageExtension
3636
}
3737

@@ -160,7 +160,7 @@ func TestArtifact_InspectRekorAttestation(t *testing.T) {
160160
img := &fakeImage{
161161
name: tt.fields.imageName,
162162
repoDigests: tt.fields.repoDigests,
163-
FakeImage: fi,
163+
Image: fi,
164164
}
165165
a, err := image2.NewArtifact(img, mockCache, tt.artifactOpt)
166166
require.NoError(t, err)
@@ -304,7 +304,7 @@ func TestArtifact_inspectOCIReferrerSBOM(t *testing.T) {
304304
img := &fakeImage{
305305
name: tt.fields.imageName,
306306
repoDigests: tt.fields.repoDigests,
307-
FakeImage: fi,
307+
Image: fi,
308308
}
309309
a, err := image2.NewArtifact(img, mockCache, tt.artifactOpt)
310310
require.NoError(t, err)

0 commit comments

Comments
 (0)