From e4c7c6dc417d5079803d77dfa5b42fa626227e2f Mon Sep 17 00:00:00 2001 From: Justin Chadwell Date: Tue, 7 Feb 2023 13:09:06 +0000 Subject: [PATCH 1/5] exporter: canonicalize sbom file paths during search Signed-off-by: Justin Chadwell --- exporter/containerimage/attestations.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/exporter/containerimage/attestations.go b/exporter/containerimage/attestations.go index 782c18733035..af3962393187 100644 --- a/exporter/containerimage/attestations.go +++ b/exporter/containerimage/attestations.go @@ -172,6 +172,8 @@ func newFileLayerFinder(target cache.ImmutableRef, remote *solver.Remote) (fileL // // 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 @@ -188,6 +190,8 @@ func (c *fileLayerFinder) find(ctx context.Context, s session.Group, filename st found := false for _, f := range files { + f = filepath.Join("/", f) + if strings.HasPrefix(f, ".wh.") { // skip whiteout files, we only care about file creations continue From 32dc0ece694692f0845737029f778b1ff25e2325 Mon Sep 17 00:00:00 2001 From: Justin Chadwell Date: Tue, 7 Feb 2023 13:09:37 +0000 Subject: [PATCH 2/5] exporter: fix file layer finder whiteout detection Signed-off-by: Justin Chadwell --- exporter/containerimage/attestations.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/exporter/containerimage/attestations.go b/exporter/containerimage/attestations.go index af3962393187..d41a657bd7f7 100644 --- a/exporter/containerimage/attestations.go +++ b/exporter/containerimage/attestations.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "io/fs" + "path/filepath" "strings" intoto "github.com/in-toto/in-toto-golang/in_toto" @@ -192,7 +193,7 @@ func (c *fileLayerFinder) find(ctx context.Context, s session.Group, filename st for _, f := range files { f = filepath.Join("/", f) - if strings.HasPrefix(f, ".wh.") { + if strings.HasPrefix(filepath.Base(f), ".wh.") { // skip whiteout files, we only care about file creations continue } From f3db1142cd3b4524926db5c1c41fd98de2691d0e Mon Sep 17 00:00:00 2001 From: Justin Chadwell Date: Tue, 7 Feb 2023 15:50:44 +0000 Subject: [PATCH 3/5] exporter: fix supplement sboms on empty scratch layer Signed-off-by: Justin Chadwell --- exporter/containerimage/attestations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/exporter/containerimage/attestations.go b/exporter/containerimage/attestations.go index d41a657bd7f7..8d28675e07d8 100644 --- a/exporter/containerimage/attestations.go +++ b/exporter/containerimage/attestations.go @@ -31,6 +31,9 @@ var intotoPlatform ocispecs.Platform = ocispecs.Platform{ // 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 } From 257fb65c9cf0e1f63d98e326713b80b5224f10b5 Mon Sep 17 00:00:00 2001 From: Justin Chadwell Date: Tue, 7 Feb 2023 17:00:18 +0000 Subject: [PATCH 4/5] exporter: fix sbom supplement core detection Signed-off-by: Justin Chadwell --- exporter/containerimage/attestations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/containerimage/attestations.go b/exporter/containerimage/attestations.go index 8d28675e07d8..a41c6039f0ba 100644 --- a/exporter/containerimage/attestations.go +++ b/exporter/containerimage/attestations.go @@ -44,7 +44,7 @@ func supplementSBOM(ctx context.Context, s session.Group, target cache.Immutable if !ok { return att, nil } - if n, _, _ := strings.Cut(att.Path, "."); n != string(name) { + if n, _, _ := strings.Cut(filepath.Base(att.Path), "."); n != string(name) { return att, nil } From 1c55dc2c58130810579dcc8f82a29d8ee5e4c792 Mon Sep 17 00:00:00 2001 From: Justin Chadwell Date: Tue, 7 Feb 2023 17:02:21 +0000 Subject: [PATCH 5/5] client: add tests for layerID in comment field Signed-off-by: Justin Chadwell --- client/client_test.go | 150 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/client/client_test.go b/client/client_test.go index 02b7ff4766a3..ba712a2ab39f 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -62,6 +62,7 @@ import ( digest "github.com/opencontainers/go-digest" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" + spdx "github.com/spdx/tools-golang/spdx/v2_3" "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh/agent" "golang.org/x/sync/errgroup" @@ -192,6 +193,7 @@ func TestIntegration(t *testing.T) { testAttestationBundle, testSBOMScan, testSBOMScanSingleRef, + testSBOMSupplements, testMultipleCacheExports, testMountStubsDirectory, testMountStubsTimestamp, @@ -8312,6 +8314,154 @@ EOF require.Subset(t, attest.Predicate, map[string]interface{}{"name": "fallback"}) } +func testSBOMSupplements(t *testing.T, sb integration.Sandbox) { + integration.CheckFeatureCompat(t, sb, integration.FeatureDirectPush, integration.FeatureSBOM) + requiresLinux(t) + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + + p := platforms.MustParse("linux/amd64") + pk := platforms.Format(p) + + frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + res := gateway.NewResult() + + // build image + st := llb.Scratch().File( + llb.Mkfile("/foo", 0600, []byte{}), + ) + def, err := st.Marshal(ctx) + if err != nil { + return nil, err + } + r, err := c.Solve(ctx, gateway.SolveRequest{ + Definition: def.ToPB(), + }) + if err != nil { + return nil, err + } + ref, err := r.SingleRef() + if err != nil { + return nil, err + } + _, err = ref.ToState() + if err != nil { + return nil, err + } + res.AddRef(pk, ref) + + expPlatforms := &exptypes.Platforms{ + Platforms: []exptypes.Platform{{ID: pk, Platform: p}}, + } + dt, err := json.Marshal(expPlatforms) + if err != nil { + return nil, err + } + res.AddMeta(exptypes.ExporterPlatformsKey, dt) + + // build attestations + doc := spdx.Document{ + SPDXIdentifier: "DOCUMENT", + Files: []*spdx.File{ + { + // foo exists... + FileSPDXIdentifier: "SPDXRef-File-foo", + FileName: "/foo", + }, + { + // ...but bar doesn't + FileSPDXIdentifier: "SPDXRef-File-bar", + FileName: "/bar", + }, + }, + } + docBytes, err := json.Marshal(doc) + if err != nil { + return nil, err + } + st = llb.Scratch(). + File(llb.Mkfile("/result.spdx", 0600, docBytes)) + def, err = st.Marshal(ctx) + if err != nil { + return nil, err + } + r, err = c.Solve(ctx, gateway.SolveRequest{ + Definition: def.ToPB(), + }) + if err != nil { + return nil, err + } + refAttest, err := r.SingleRef() + if err != nil { + return nil, err + } + _, err = ref.ToState() + if err != nil { + return nil, err + } + + res.AddAttestation(pk, gateway.Attestation{ + Kind: gatewaypb.AttestationKindInToto, + Ref: refAttest, + Path: "/result.spdx", + InToto: result.InTotoAttestation{ + PredicateType: intoto.PredicateSPDX, + }, + Metadata: map[string][]byte{ + result.AttestationSBOMCore: []byte("result"), + }, + }) + + return res, nil + } + + // test the default fallback scanner + target := registry + "/buildkit/testsbom:latest" + _, err = c.Build(sb.Context(), SolveOpt{ + FrontendAttrs: map[string]string{ + "attest:sbom": "", + }, + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + }, "", frontend, nil) + require.NoError(t, err) + + desc, provider, err := contentutil.ProviderFromRef(target) + require.NoError(t, err) + + imgs, err := testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Equal(t, 2, len(imgs.Images)) + + att := imgs.Find("unknown/unknown") + attest := struct { + intoto.StatementHeader + Predicate spdx.Document + }{} + require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) + require.Equal(t, "https://in-toto.io/Statement/v0.1", attest.Type) + require.Equal(t, intoto.PredicateSPDX, attest.PredicateType) + + require.Equal(t, "DOCUMENT", string(attest.Predicate.SPDXIdentifier)) + require.Len(t, attest.Predicate.Files, 2) + require.Equal(t, attest.Predicate.Files[0].FileName, "/foo") + require.Regexp(t, "^layerID: sha256:", attest.Predicate.Files[0].FileComment) + require.Equal(t, attest.Predicate.Files[1].FileName, "/bar") + require.Empty(t, attest.Predicate.Files[1].FileComment) +} + func testMultipleCacheExports(t *testing.T, sb integration.Sandbox) { integration.CheckFeatureCompat(t, sb, integration.FeatureMultiCacheExport) c, err := New(sb.Context(), sb.Address())