Skip to content

Commit 205f7cc

Browse files
SCALIBR Teamcopybara-github
SCALIBR Team
authored andcommitted
internal
PiperOrigin-RevId: 755147565
1 parent eb23a48 commit 205f7cc

File tree

5 files changed

+141
-2
lines changed

5 files changed

+141
-2
lines changed

artifact/image/layerscanning/image/image.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package image
1818

1919
import (
20+
"context"
2021
"errors"
2122
"fmt"
2223
"io"
@@ -28,6 +29,7 @@ import (
2829

2930
"archive/tar"
3031

32+
"github.com/docker/docker/client"
3133
v1 "github.com/google/go-containerregistry/pkg/v1"
3234
"github.com/google/go-containerregistry/pkg/v1/remote"
3335
"github.com/google/go-containerregistry/pkg/v1/tarball"
@@ -48,6 +50,9 @@ const (
4850
// "8 is __POSIX_SYMLOOP_MAX (the minimum allowed value for SYMLOOP_MAX), and a common limit".
4951
DefaultMaxSymlinkDepth = 6
5052

53+
dockerImageNameSeparator = ":"
54+
tarFileNameSeparator = "_"
55+
5156
// filePermission represents the permission bits for a file, which are minimal since files in the
5257
// layer scanning use case are read-only.
5358
filePermission = 0600
@@ -159,6 +164,89 @@ func FromRemoteName(imageName string, config *Config, imageOptions ...remote.Opt
159164
return FromV1Image(v1Image, config)
160165
}
161166

167+
// CreateTarBallFromImage creates a tarball from a local docker image. This is the API version of 'docker save image' command
168+
func createTarBallFromImage(imageName string) (string, error) {
169+
170+
dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
171+
if err != nil {
172+
return "", fmt.Errorf("unable to create docker client to untar image %s: %w", imageName, err)
173+
}
174+
inputStream, err := dockerClient.ImageSave(context.Background(), []string{imageName})
175+
if err != nil {
176+
return "", fmt.Errorf("unable to create docker stream to untar image %s: %w", imageName, err)
177+
}
178+
defer inputStream.Close()
179+
tarFileName := strings.ReplaceAll(imageName, dockerImageNameSeparator, tarFileNameSeparator) + ".tar"
180+
log.Infof("Tarfile name is %s", tarFileName)
181+
fileFd, err := os.CreateTemp("", tarFileName)
182+
if err != nil {
183+
return "", fmt.Errorf("unable to create file to untar image %s: %w", imageName, err)
184+
}
185+
_, err = io.Copy(fileFd, inputStream)
186+
if err != nil {
187+
fileFd.Close()
188+
errVal := os.Remove(fileFd.Name())
189+
if !os.IsNotExist(errVal) {
190+
log.Warnf("Unable to remove file %s: %w", fileFd.Name(), errVal)
191+
}
192+
return "", fmt.Errorf("unable to write to tarfile for image %s: %w", imageName, err)
193+
}
194+
fileFd.Close()
195+
return fileFd.Name(), nil
196+
}
197+
198+
// Check if the imageName is of the form imageName:imageTag
199+
func validateImageNameAndTag(imageName string) (bool, error) {
200+
dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
201+
if err != nil {
202+
return false, err
203+
}
204+
defer dockerClient.Close()
205+
_, _, err = dockerClient.ImageInspectWithRaw(context.Background(), imageName)
206+
if err != nil {
207+
if client.IsErrNotFound(err) {
208+
return false, nil
209+
}
210+
return false, err
211+
}
212+
return true, nil
213+
}
214+
215+
// FromLocalDockerImage creates a tarball from a docker image which is on a local hard disk
216+
// We convert the image to a tarball, and then pass it to the FromTarball function which does all that is necessary
217+
func FromLocalDockerImage(imageName string, config *Config) (*Image, error) {
218+
219+
var tarBallName string
220+
var img *Image
221+
// First, the image name *MUST* always be of the form imageName:imageTag
222+
if !strings.Contains(imageName, dockerImageNameSeparator) {
223+
return nil, fmt.Errorf("Image name MUST be specified with a tag and be of the form <image_name>:<tag>")
224+
}
225+
// Now, check if the image actually exists on the local hard disk
226+
imageExists, err := validateImageNameAndTag(imageName)
227+
if err != nil {
228+
return nil, fmt.Errorf("Image %s error while trying to access it: %w", imageName, err)
229+
}
230+
if !imageExists {
231+
return nil, fmt.Errorf("Image %s does not exist", imageName)
232+
}
233+
// Now, create a tarball out of the image by using the API equivalent of 'docker save image:tag'
234+
tarBallName, err = createTarBallFromImage(imageName)
235+
if err != nil {
236+
errVal := os.Remove(tarBallName)
237+
if !os.IsNotExist(errVal) {
238+
log.Warnf("Unable to remove file %s: %w", tarBallName, errVal)
239+
}
240+
return nil, fmt.Errorf("Unable to use image %s: %w", imageName, err)
241+
}
242+
img, err = FromTarball(tarBallName, config)
243+
err = os.Remove(tarBallName)
244+
if err != nil {
245+
log.Warnf("Unable to remove the tarball %s: %v", tarBallName, err)
246+
}
247+
return img, err
248+
}
249+
162250
// FromTarball creates an Image from a tarball file that stores a container image.
163251
func FromTarball(tarPath string, config *Config) (*Image, error) {
164252
// TODO b/381251067: Look into supporting OCI images.

binary/cli/cli.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ type Flags struct {
136136
MaxFileSize int
137137
UseGitignore bool
138138
RemoteImage string
139+
ImageLocal string
139140
ImageTarball string
140141
ImagePlatform string
141142
GoBinaryVersionFromContent bool
@@ -180,6 +181,12 @@ func ValidateFlags(flags *Flags) error {
180181
if flags.ImageTarball != "" && flags.ImagePlatform != "" {
181182
return errors.New("--image-tarball cannot be used with --image-platform")
182183
}
184+
if flags.ImageLocal != "" && flags.RemoteImage != "" {
185+
return errors.New("--image-local cannot be used with --remote-image")
186+
}
187+
if flags.ImageLocal != "" && flags.ImagePlatform != "" {
188+
return errors.New("--image-local cannot be used with --image-platform")
189+
}
183190
if err := validateResultPath(flags.ResultFile); err != nil {
184191
return fmt.Errorf("--result %w", err)
185192
}
@@ -534,6 +541,11 @@ func (f *Flags) scanRoots() ([]*scalibrfs.ScanRoot, error) {
534541
if f.ImageTarball != "" {
535542
return nil, nil
536543
}
544+
// If ImageLocal is set, do not set the root.
545+
// It is computed later on by ScanContainer(...) when the tarball is read.
546+
if f.ImageLocal != "" {
547+
return nil, nil
548+
}
537549

538550
// Compute the default scan roots.
539551
var scanRoots []*scalibrfs.ScanRoot

binary/cli/cli_test.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,14 +210,23 @@ func TestValidateFlags(t *testing.T) {
210210
wantErr: nil,
211211
},
212212
{
213-
desc: "Remoe Image with Image Tarball",
213+
desc: "Remote Image with Image Tarball",
214214
flags: &cli.Flags{
215215
RemoteImage: "docker",
216216
ImageTarball: "image.tar",
217217
ResultFile: "result.textproto",
218218
},
219219
wantErr: cmpopts.AnyError,
220220
},
221+
{
222+
desc: "Local Docker Image",
223+
flags: &cli.Flags{
224+
RemoteImage: "docker",
225+
ImageLocal: "nginx:latest",
226+
ResultFile: "result.textproto",
227+
},
228+
wantErr: cmpopts.AnyError,
229+
},
221230
} {
222231
t.Run(tc.desc, func(t *testing.T) {
223232
err := cli.ValidateFlags(tc.flags)
@@ -273,6 +282,19 @@ func TestGetScanConfig_ScanRoots(t *testing.T) {
273282
"windows": nil,
274283
},
275284
},
285+
{
286+
desc: "Scan root is null if local image is provided",
287+
flags: map[string]*cli.Flags{
288+
"darwin": {ImageLocal: "nginx:latest"},
289+
"linux": {ImageLocal: "nginx:latest"},
290+
"windows": {ImageLocal: "nginx:latest"},
291+
},
292+
wantScanRoots: map[string][]string{
293+
"darwin": nil,
294+
"linux": nil,
295+
"windows": nil,
296+
},
297+
},
276298
} {
277299
t.Run(tc.desc, func(t *testing.T) {
278300
wantScanRoots, ok := tc.wantScanRoots[runtime.GOOS]

binary/scalibr/scalibr.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ func parseFlags(args []string) (*cli.Flags, error) {
7575
useGitignore := fs.Bool("use-gitignore", false, "Skip files declared in .gitignore files in source repos.")
7676
remoteImage := fs.String("remote-image", "", "The remote image to scan. If specified, SCALIBR pulls and scans this image instead of the local filesystem.")
7777
imageTarball := fs.String("image-tarball", "", "The path to a tarball containing a container image. These are commonly procuded using `docker save`. If specified, SCALIBR scans this image instead of the local filesystem.")
78+
imageDockerLocal := fs.String("image-local-docker", "", "The docker image that is available in the local filesystem. These are the images from the output of \"docker image ls\". If specified, SCALIBR scans this image. The name of the image MUST also include the tag of the image <image_name>:<image_tag>.")
7879
imagePlatform := fs.String("image-platform", "", "The platform of the remote image to scan. If not specified, the platform of the client is used. Format is os/arch (e.g. linux/arm64)")
7980
goBinaryVersionFromContent := fs.Bool("gobinary-version-from-content", false, "Parse the main module version from the binary content. Off by default because this drastically increases latency (~10x).")
8081
govulncheckDBPath := fs.String("govulncheck-db", "", "Path to the offline DB for the govulncheck detectors to use. Leave empty to run the detectors in online mode.")
@@ -112,6 +113,7 @@ func parseFlags(args []string) (*cli.Flags, error) {
112113
MaxFileSize: *maxFileSize,
113114
UseGitignore: *useGitignore,
114115
RemoteImage: *remoteImage,
116+
ImageLocal: *imageDockerLocal,
115117
ImageTarball: *imageTarball,
116118
ImagePlatform: *imagePlatform,
117119
GoBinaryVersionFromContent: *goBinaryVersionFromContent,

binary/scanrunner/scanrunner.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,24 @@ func RunScan(flags *cli.Flags) int {
6060
log.Errorf("Failed to create image from tarball: %v", err)
6161
return 1
6262
}
63+
defer img.CleanUp()
6364
result, err = scalibr.New().ScanContainer(context.Background(), img, cfg)
6465
if err != nil {
65-
log.Errorf("Failed to scan container: %v", err)
66+
log.Errorf("Failed to scan tarball: %v", err)
67+
return 1
68+
}
69+
} else if flags.ImageLocal != "" { // We will scan an image in the local hard disk
70+
layerCfg := scalibrlayerimage.DefaultConfig()
71+
log.Infof("Scanning local image: %s", flags.ImageLocal)
72+
img, err := scalibrlayerimage.FromLocalDockerImage(flags.ImageLocal, layerCfg)
73+
if err != nil {
74+
log.Errorf("Failed to scan local image: %v", err)
75+
return 1
76+
}
77+
defer img.CleanUp()
78+
result, err = scalibr.New().ScanContainer(context.Background(), img, cfg)
79+
if err != nil {
80+
log.Errorf("Failed to scan local image: %v", err)
6681
return 1
6782
}
6883
} else {

0 commit comments

Comments
 (0)