forked from google/osv-scalibr
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathimage.go
144 lines (134 loc) · 5.45 KB
/
image.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package image provides functionality to scan a container image by layers for software
// inventory.
package image
import (
"fmt"
"io"
"os"
"strings"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/osv-scalibr/artifact/image/require"
"github.com/google/osv-scalibr/artifact/image/unpack"
"github.com/opencontainers/go-digest"
scalibrfs "github.com/google/osv-scalibr/fs"
)
// Layer is a filesystem derived from a container layer that can be scanned for software inventory.
// It also holds metadata about the container layer such as whether it is empty, its diffID, index,
// and command.
type Layer interface {
// FS outputs a filesystem that consist of the files found in the layer. This includes files that
// were added or modified. Whiteout files are also included in the filesystem if files or
// directories from previous layers were removed.
FS() scalibrfs.FS
// IsEmpty signifies whether the layer is empty. This should correspond with an empty filesystem
// produced by the FS method.
IsEmpty() bool
// DiffID is the hash of the uncompressed layer. Will be an empty string if the layer is empty.
DiffID() digest.Digest
// Command is the specific command that produced the layer.
Command() string
// Uncompressed gives the uncompressed tar as a file reader.
Uncompressed() (io.ReadCloser, error)
}
// ChainLayer is a filesystem derived from container layers that can be scanned for software
// inventory. It holds all the files found in layer 0, layer 1, ..., layer n (where n is the layer
// index). It also holds metadata about the latest container layer such as whether it is empty, its
// diffID, command, and index.
type ChainLayer interface {
// FS output an filesystem that consist of the files found in the layer n and all previous layers
// (layer 0, layer 1, ..., layer n).
FS() scalibrfs.FS
// Index is the index of the latest layer in the layer chain.
Index() int
// Layer is the latest layer in the layer chain.
Layer() Layer
}
// Image is a container image that can be scanned for software inventory. It is composed of a set of
// layers that can be scanned for software inventory.
type Image interface {
Layer(index int) (Layer, error)
Layers() ([]Layer, error)
ChainLayer(index int) (ChainLayer, error)
ChainLayers() ([]ChainLayer, error)
ConfigFile() *v1.ConfigFile
FileHistory(filepath string) History
}
// V1ImageFromRemoteName creates a v1.Image from a remote container image name.
func V1ImageFromRemoteName(imageName string, imageOptions ...remote.Option) (v1.Image, error) {
imageName = strings.TrimPrefix(imageName, "https://")
var image v1.Image
if strings.Contains(imageName, "@") {
// Pull from a digest name.
ref, err := name.NewDigest(strings.TrimPrefix(imageName, "https://"))
if err != nil {
return nil, fmt.Errorf("unable to parse digest: %w", err)
}
descriptor, err := remote.Get(ref, imageOptions...)
if err != nil {
return nil, fmt.Errorf("couldn’t pull remote image %s: %v", ref, err)
}
image, err = descriptor.Image()
if err != nil {
return nil, fmt.Errorf("couldn’t parse image manifest %s: %v", ref, err)
}
} else {
// Pull from a tag.
tag, err := name.NewTag(strings.TrimPrefix(imageName, "https://"))
if err != nil {
return nil, fmt.Errorf("unable to parse image reference: %w", err)
}
image, err = remote.Image(tag, imageOptions...)
if err != nil {
return nil, fmt.Errorf("couldn’t pull remote image %s: %v", tag, err)
}
}
return image, nil
}
// NewFromRemoteName pulls a remote container and creates a
// SCALIBR filesystem for scanning it.
func NewFromRemoteName(imageName string, imageOptions ...remote.Option) (scalibrfs.FS, error) {
image, err := V1ImageFromRemoteName(imageName, imageOptions...)
if err != nil {
return nil, fmt.Errorf("failed to load image from remote name %q: %w", imageName, err)
}
return NewFromImage(image)
}
// NewFromImage creates a SCALIBR filesystem for scanning a container
// from its image descriptor.
func NewFromImage(image v1.Image) (scalibrfs.FS, error) {
outDir, err := os.MkdirTemp(os.TempDir(), "scalibr-container-")
if err != nil {
return nil, fmt.Errorf("couldn’t create tmp dir for image: %v", err)
}
// Squash the image's final layer into a directory.
cfg := &unpack.UnpackerConfig{
SymlinkResolution: unpack.SymlinkRetain,
SymlinkErrStrategy: unpack.SymlinkErrLog,
MaxPass: unpack.DefaultMaxPass,
MaxFileBytes: unpack.DefaultMaxFileBytes,
Requirer: &require.FileRequirerAll{},
}
unpacker, err := unpack.NewUnpacker(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create image unpacker: %w", err)
}
if err = unpacker.UnpackSquashed(outDir, image); err != nil {
return nil, fmt.Errorf("failed to unpack image into directory %q: %w", outDir, err)
}
return scalibrfs.DirFS(outDir), nil
}