Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(image): prevent scanning oversized container images #8178

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

nikpivkin
Copy link
Contributor

@nikpivkin nikpivkin commented Dec 25, 2024

Description

This PR adds the --max-image-size flag. If the flag is specified and the uncompressed image size exceeds the maximum size, Trivy exits with an error. If the flag is not specified, the behaviour does not change.

For more implementation details, see #8176

Related issues

Checklist

  • I've read the guidelines for contributing to this repository.
  • I've followed the conventions in the PR title.
  • I've added tests that prove my fix is effective or that my feature works.
  • I've updated the documentation with the relevant information (if needed).
  • I've added usage information (if the PR introduces new options)
  • I've included a "before" and "after" example to the description (if the PR is a user interface change).

@nikpivkin nikpivkin marked this pull request as ready for review December 26, 2024 09:36
@nikpivkin
Copy link
Contributor Author

@knqyf263 I was thinking that we could download the compressed layer and then decompress it ourselves. I saw that downloader.Download is used for decompression. Does it support all the compression types that go-containerregistry does?

@knqyf263
Copy link
Collaborator

This is what was in my mind.

	layer, err := a.image.LayerByDiffID(h)
	if err != nil {...}

	rc, err := layer.Uncompressed()
	if err != nil {...}

	f, err := os.Open(diffID + ".tar")
	if err != nil{...}

	io.Copy(f, rc)

@nikpivkin
Copy link
Contributor Author

@knqyf263 Then we can't display the progress bar correctly, because we don't know the size of the uncompressed layer.

@knqyf263
Copy link
Collaborator

@knqyf263
Copy link
Collaborator

If it doesn't work, we can use layer.Compressed, inject the progress bar and re-implement uncompressed logic.
https://github.com/google/go-containerregistry/blob/6bce25ecf0297c1aa9072bc665b5cf58d53e1c54/pkg/v1/partial/compressed.go#L50-L78

@nikpivkin
Copy link
Contributor Author

@knqyf263 I kept the old behavior, if no flag is passed, to avoid unnecessary disk operations. But then the progress bar will be displayed only when the flag is passed. What do you think?

pkg/fanal/artifact/image/image.go Outdated Show resolved Hide resolved
pkg/fanal/artifact/image/image.go Outdated Show resolved Hide resolved
@knqyf263
Copy link
Collaborator

@nikpivkin I got an idea.
https://go.dev/play/p/fY8jM99don_r

@knqyf263
Copy link
Collaborator

This might be better.

// ProgressLayer wraps a v1.Layer to add progress bar functionality
type ProgressLayer struct {
	v1.Layer
}

func NewProgressLayer(layer v1.Layer) *ProgressLayer {
	return &ProgressLayer{ Layer: layer, }
}

func (l *ProgressLayer) Compressed() (io.ReadCloser, error) {
	size, err := l.Layer.Size()
	if err != nil {
		return nil, err
	}

	rc, err := l.Layer.Compressed()
	if err != nil {
		return nil, err
	}

	bar := pb.Full.Start64(size)
	bar.Set(pb.Bytes, true)

	return bar.NewProxyReader(rc), nil
}

@knqyf263
Copy link
Collaborator

knqyf263 commented Dec 27, 2024

Even better

// progressLayer wraps a v1.Layer to add progress bar functionality
type progressLayer struct {
	v1.Layer
}

func (l *progressLayer) Compressed() (io.ReadCloser, error) {
	rc, err := l.Layer.Compressed()
	if err != nil {
		return nil, err
	}

	size, err := l.Layer.Size()
	if err != nil {
		return nil, err
	}

	bar := pb.Full.Start64(size)
	bar.Set(pb.Bytes, true)

	return bar.NewProxyReader(rc), nil
}

func NewProgressLayer(layer v1.Layer) (v1.Layer, error) {
	return partial.CompressedToLayer(&progressLayer{ Layer: layer})
}

@nikpivkin
Copy link
Contributor Author

@knqyf263 Ok, I'll think about how to use this with the progress bar pool otherwise there is flickering in the terminal when loading in parallel

@knqyf263
Copy link
Collaborator

@nikpivkin Actually, the progress bar should be discussed in #8186. I'll re-post it there.

cmd/trivy/main.go Outdated Show resolved Hide resolved
cmd/trivy/main.go Outdated Show resolved Hide resolved
pkg/fanal/artifact/image/image.go Show resolved Hide resolved
pkg/fanal/artifact/image/image.go Outdated Show resolved Hide resolved
pkg/fanal/artifact/image/image.go Show resolved Hide resolved
@knqyf263
Copy link
Collaborator

knqyf263 commented Jan 6, 2025

BTW, Trivy also supports VM images. I'm unsure, but users may want to introduce the same functionality in the future. Do you think we want to rename --max-image-size to --max-target-size or something like that? Our VM scanning also targets VM "images". --max-image-size may be fine.

@DmitriyLewen
Copy link
Contributor

I think that --max-target-size will confuse users, because they will try to use this flag in fs mode.

I think --max-image-size is good name.

Also i suggest to add this flag for VM scan in another PR.

@knqyf263
Copy link
Collaborator

knqyf263 commented Jan 17, 2025

I performed a quick test on Azure, which has a stable network. The current approach (without --max-image-size) seems faster. I'm sure we'll get different results depending on the environment, though.
https://gist.github.com/knqyf263/e0e4fa5bd1359fd9e68408f3c5244f1a?permalink_comment_id=5393170#gistcomment-5393170

Comment on lines 104 to 107
if err := os.RemoveAll(a.layerCacheDir); err != nil {
log.Error("Failed to remove layer cache", log.Err(err))
}
return artifact.Reference{}, err
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can delete the cache directory if checkImageSize returns an error, but how about other errors? If I understand correctly, Artifact.Clean() will not be called.

So, should we call the defer function before handling the error?

trivy/pkg/scanner/scan.go

Lines 160 to 165 in ec124bd

defer func() {
if err := s.artifact.Clean(artifactInfo); err != nil {
log.Warn("Failed to clean the artifact",
log.String("artifact", artifactInfo.Name), log.Err(err))
}
}()

Copy link
Contributor Author

@nikpivkin nikpivkin Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean takes artifactInfo, and if we call it before the error is checked, the artifactInfo will not be valid. Maybe we should clear the cache inside Inspect if any error occurred?

	defer func() {
		if err != nil {
			if rerr := os.RemoveAll(a.layerCacheDir); rerr != nil {
				log.Error("Failed to remove layer cache", log.Err(rerr))
			}
		}
	}()

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought using Clean() is more straightforward, but you're actually right. We don't need this cache after Inspect is complete. It makes more sense to delete it in the method.

@nikpivkin nikpivkin requested a review from knqyf263 January 17, 2025 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat: prevent scanning oversized container images
3 participants