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 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
|
package containerimage
import (
"bytes"
"context"
"fmt"
"io/fs"
"path/filepath"
"strings"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/moby/buildkit/cache"
"github.com/moby/buildkit/exporter"
"github.com/moby/buildkit/exporter/attestation"
gatewaypb "github.com/moby/buildkit/frontend/gateway/pb"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/solver"
"github.com/moby/buildkit/solver/result"
"github.com/moby/buildkit/version"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
spdx_json "github.com/spdx/tools-golang/json"
"github.com/spdx/tools-golang/spdx"
"github.com/spdx/tools-golang/spdx/v2/common"
)
var intotoPlatform = ocispecs.Platform{
Architecture: "unknown",
OS: "unknown",
}
// supplementSBOM modifies SPDX attestations to include the file layers
func supplementSBOM(ctx context.Context, s session.Group, target cache.ImmutableRef, targetRemote *solver.Remote, att exporter.Attestation) (exporter.Attestation, error) {
if target == nil {
return att, nil
}
if att.Kind != gatewaypb.AttestationKindInToto {
return att, nil
}
if att.InToto.PredicateType != intoto.PredicateSPDX {
return att, nil
}
name, ok := att.Metadata[result.AttestationSBOMCore]
if !ok {
return att, nil
}
if n, _, _ := strings.Cut(filepath.Base(att.Path), "."); n != string(name) {
return att, nil
}
content, err := attestation.ReadAll(ctx, s, att)
if err != nil {
return att, err
}
doc, err := decodeSPDX(content)
if err != nil {
// ignore decoding error
return att, nil
}
layers, err := newFileLayerFinder(target, targetRemote)
if err != nil {
return att, err
}
modifyFile := func(f *spdx.File) error {
if f == nil {
// Skip over nil entries - this is likely a bug in the SPDX parser,
// but we shouldn't accidentally panic if we encounter it.
return nil
}
if f.FileComment != "" {
// Skip over files that already have a comment - since the data is
// unstructured, we can't correctly overwrite this field without
// possibly breaking some scanner functionality.
return nil
}
_, desc, err := layers.find(ctx, s, f.FileName)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
return nil
}
f.FileComment = fmt.Sprintf("layerID: %s", desc.Digest.String())
return nil
}
for _, f := range doc.Files {
if err := modifyFile(f); err != nil {
return att, err
}
}
for _, p := range doc.Packages {
for _, f := range p.Files {
if err := modifyFile(f); err != nil {
return att, err
}
}
}
if doc.CreationInfo == nil {
doc.CreationInfo = &spdx.CreationInfo{}
}
doc.CreationInfo.Creators = append(doc.CreationInfo.Creators, common.Creator{
CreatorType: "Tool",
Creator: "buildkit-" + version.Version,
})
content, err = encodeSPDX(doc)
if err != nil {
return att, err
}
return exporter.Attestation{
Kind: att.Kind,
Path: att.Path,
ContentFunc: func() ([]byte, error) { return content, nil },
InToto: att.InToto,
}, nil
}
func decodeSPDX(dt []byte) (s *spdx.Document, err error) {
doc, err := spdx_json.Read(bytes.NewReader(dt))
if err != nil {
return nil, errors.Wrap(err, "unable to decode spdx")
}
if doc == nil {
return nil, errors.New("decoding produced empty spdx document")
}
return doc, nil
}
func encodeSPDX(s *spdx.Document) (dt []byte, err error) {
w := bytes.NewBuffer(nil)
err = spdx_json.Write(s, w)
if err != nil {
return nil, errors.Wrap(err, "unable to encode spdx")
}
return w.Bytes(), nil
}
// fileLayerFinder finds the layer that contains a file, with caching to avoid
// repeated FileList lookups.
type fileLayerFinder struct {
pending []fileLayerEntry
cache map[string]fileLayerEntry
}
type fileLayerEntry struct {
ref cache.ImmutableRef
desc ocispecs.Descriptor
}
func newFileLayerFinder(target cache.ImmutableRef, remote *solver.Remote) (fileLayerFinder, error) {
chain := target.LayerChain()
descs := remote.Descriptors
if len(chain) != len(descs) {
return fileLayerFinder{}, errors.New("layer chain and descriptor list are not the same length")
}
pending := make([]fileLayerEntry, len(chain))
for i, ref := range chain {
pending[i] = fileLayerEntry{ref: ref, desc: descs[i]}
}
return fileLayerFinder{
pending: pending,
cache: map[string]fileLayerEntry{},
}, nil
}
// find finds the layer that contains the file, returning the ImmutableRef and
// descriptor for the layer. If the file searched for was deleted, find returns
// the layer that created the file, not the one that deleted it.
//
// find is not concurrency-safe.
func (c *fileLayerFinder) find(ctx context.Context, s session.Group, filename string) (cache.ImmutableRef, *ocispecs.Descriptor, error) {
filename = filepath.Join("/", filename)
// return immediately if we've already found the layer containing filename
if cache, ok := c.cache[filename]; ok {
return cache.ref, &cache.desc, nil
}
for len(c.pending) > 0 {
// pop the last entry off the pending list (we traverse the layers backwards)
pending := c.pending[len(c.pending)-1]
files, err := pending.ref.FileList(ctx, s)
if err != nil {
return nil, nil, err
}
c.pending = c.pending[:len(c.pending)-1]
found := false
for _, f := range files {
f = filepath.Join("/", f)
if strings.HasPrefix(filepath.Base(f), ".wh.") {
// skip whiteout files, we only care about file creations
continue
}
// add all files in this layer to the cache
if _, ok := c.cache[f]; ok {
continue
}
c.cache[f] = pending
// if we found the file, return the layer (but finish populating the cache first)
if f == filename {
found = true
}
}
if found {
return pending.ref, &pending.desc, nil
}
}
return nil, nil, fs.ErrNotExist
}
|