-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
attestations.go
220 lines (195 loc) · 5.7 KB
/
attestations.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
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
}