diff --git a/cmd/buildctl/build.go b/cmd/buildctl/build.go index 223bc3076673..3f11b3bd7512 100644 --- a/cmd/buildctl/build.go +++ b/cmd/buildctl/build.go @@ -278,26 +278,13 @@ func buildAction(clicontext *cli.Context) error { } }() - solveAttr := map[string]string{} - frontendAttr := map[string]string{} - for k, v := range solveOpt.FrontendAttrs { - if strings.HasPrefix(k, "attest:") || strings.HasPrefix(k, "build-arg:BUILDKIT_ATTEST_") { - frontendAttr[k] = v - } else { - solveAttr[k] = v - } - } - sreq := gateway.SolveRequest{ Frontend: solveOpt.Frontend, - FrontendOpt: solveAttr, + FrontendOpt: solveOpt.FrontendAttrs, } if def != nil { sreq.Definition = def.ToPB() } - solveOpt.Frontend = "" - solveOpt.FrontendAttrs = frontendAttr - resp, err := c.Build(ctx, solveOpt, "buildctl", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { _, isSubRequest := sreq.FrontendOpt["requestid"] if isSubRequest { diff --git a/examples/dockerfile2llb/main.go b/examples/dockerfile2llb/main.go index b575765a41f0..2fd693a4ae80 100644 --- a/examples/dockerfile2llb/main.go +++ b/examples/dockerfile2llb/main.go @@ -41,7 +41,7 @@ func xmain() error { caps := pb.Caps.CapSet(pb.Caps.All()) - state, img, err := dockerfile2llb.Dockerfile2LLB(appcontext.Context(), df, dockerfile2llb.ConvertOpt{ + state, img, _, err := dockerfile2llb.Dockerfile2LLB(appcontext.Context(), df, dockerfile2llb.ConvertOpt{ MetaResolver: imagemetaresolver.Default(), Target: opt.target, LLBCaps: &caps, diff --git a/frontend/attest/sbom.go b/frontend/attestations/sbom/sbom.go similarity index 99% rename from frontend/attest/sbom.go rename to frontend/attestations/sbom/sbom.go index 6c70a3d654a9..668912b40d65 100644 --- a/frontend/attest/sbom.go +++ b/frontend/attestations/sbom/sbom.go @@ -1,4 +1,4 @@ -package attest +package sbom import ( "context" diff --git a/frontend/dockerfile/builder/build.go b/frontend/dockerfile/builder/build.go index 81c690d0aa20..0ac4ae1239ee 100644 --- a/frontend/dockerfile/builder/build.go +++ b/frontend/dockerfile/builder/build.go @@ -21,8 +21,8 @@ import ( "github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/exporter/containerimage/exptypes" "github.com/moby/buildkit/frontend" - "github.com/moby/buildkit/frontend/attest" "github.com/moby/buildkit/frontend/attestations" + "github.com/moby/buildkit/frontend/attestations/sbom" "github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb" "github.com/moby/buildkit/frontend/dockerfile/dockerignore" "github.com/moby/buildkit/frontend/dockerfile/parser" @@ -434,8 +434,9 @@ func Build(ctx context.Context, c client.Client) (_ *client.Result, err error) { return nil, err } + target := opts[keyTarget] convertOpt := dockerfile2llb.ConvertOpt{ - Target: opts[keyTarget], + Target: target, MetaResolver: c, BuildArgs: filter(opts, buildArgPrefix), Labels: filter(opts, labelPrefix), @@ -489,7 +490,7 @@ func Build(ctx context.Context, c client.Client) (_ *client.Result, err error) { } } - var scanner attest.Scanner + var scanner sbom.Scanner attests, err := attestations.Parse(opts) if err != nil { return nil, err @@ -506,11 +507,12 @@ func Build(ctx context.Context, c client.Client) (_ *client.Result, err error) { ref = reference.TagNameOnly(ref) exportMap = true - scanner, err = attest.CreateSBOMScanner(ctx, c, ref.String()) + scanner, err = sbom.CreateSBOMScanner(ctx, c, ref.String()) if err != nil { return nil, err } } + scanTargets := make([]*dockerfile2llb.SBOMTargets, len(targetPlatforms)) eg, ctx2 = errgroup.WithContext(ctx) @@ -523,8 +525,7 @@ func Build(ctx context.Context, c client.Client) (_ *client.Result, err error) { opt.Warn = nil } opt.ContextByName = contextByNameFunc(c, c.BuildOpts().SessionID) - st, img, err := dockerfile2llb.Dockerfile2LLB(ctx2, dtDockerfile, opt) - + st, img, scanTarget, err := dockerfile2llb.Dockerfile2LLB(ctx2, dtDockerfile, opt) if err != nil { return err } @@ -601,6 +602,7 @@ func Build(ctx context.Context, c client.Client) (_ *client.Result, err error) { Platform: p, } } + scanTargets[i] = scanTarget return nil }) }(i, tp) @@ -611,17 +613,8 @@ func Build(ctx context.Context, c client.Client) (_ *client.Result, err error) { } if scanner != nil { - for _, p := range expPlatforms.Platforms { - ref, ok := res.Refs[p.ID] - if !ok { - return nil, errors.Errorf("could not find ref %s", p.ID) - } - st, err := ref.ToState() - if err != nil { - return nil, err - } - - att, st, err := scanner(ctx, p.ID, st, nil) + for i, p := range expPlatforms.Platforms { + att, st, err := scanner(ctx, p.ID, scanTargets[i].Core, scanTargets[i].Extras) if err != nil { return nil, err } diff --git a/frontend/dockerfile/dockerfile2llb/convert.go b/frontend/dockerfile/dockerfile2llb/convert.go index e41a89c05dd2..130abc657c67 100644 --- a/frontend/dockerfile/dockerfile2llb/convert.go +++ b/frontend/dockerfile/dockerfile2llb/convert.go @@ -44,8 +44,16 @@ const ( emptyImageName = "scratch" defaultContextLocalName = "context" historyComment = "buildkit.dockerfile.v0" + + sbomScanContext = "BUILDKIT_SBOM_SCAN_CONTEXT" + sbomScanStage = "BUILDKIT_SBOM_SCAN_STAGE" ) +var nonEnvArgs = map[string]struct{}{ + sbomScanContext: {}, + sbomScanStage: {}, +} + type ConvertOpt struct { Target string MetaResolver llb.ImageMetaResolver @@ -77,12 +85,36 @@ type ConvertOpt struct { ContextByName func(ctx context.Context, name, resolveMode string, p *ocispecs.Platform) (*llb.State, *Image, error) } -func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (*llb.State, *Image, error) { +type SBOMTargets struct { + Core llb.State + Extras map[string]llb.State +} + +func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (*llb.State, *Image, *SBOMTargets, error) { ds, err := toDispatchState(ctx, dt, opt) if err != nil { - return nil, nil, err + return nil, nil, nil, err + } + + sbom := SBOMTargets{ + Core: ds.state, + Extras: map[string]llb.State{}, } - return &ds.state, &ds.image, nil + if ds.scanContext { + sbom.Extras["context"] = ds.opt.buildContext + } + for dsi := ds; dsi != nil; dsi = dsi.base { + if ds != dsi && dsi.scanStage { + sbom.Extras["stage:"+dsi.stageName] = dsi.state + } + for dsi2 := range dsi.deps { + if dsi2.scanStage { + sbom.Extras["stage:"+dsi2.stageName] = dsi2.state + } + } + } + + return &ds.state, &ds.image, &sbom, nil } func Dockefile2Outline(ctx context.Context, dt []byte, opt ConvertOpt) (*outline.Outline, error) { @@ -533,10 +565,23 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS return nil, parser.WithLocation(err, cmd.Location()) } } + d.opt = opt for p := range d.ctxPaths { ctxPaths[p] = struct{}{} } + + locals := []instructions.KeyValuePairOptional{} + locals = append(locals, d.opt.metaArgs...) + locals = append(locals, d.buildArgs...) + for _, a := range locals { + switch a.Key { + case sbomScanStage: + d.scanStage = isEnabledForStage(d.stageName, a.ValueString()) + case sbomScanContext: + d.scanContext = isEnabledForStage(d.stageName, a.ValueString()) + } + } } if len(opt.Labels) != 0 && target.image.Config.Labels == nil { @@ -749,6 +794,7 @@ func dispatch(d *dispatchState, cmd command, opt dispatchOpt) error { } type dispatchState struct { + opt dispatchOpt state llb.State image Image platform *ocispecs.Platform @@ -769,6 +815,8 @@ type dispatchState struct { buildInfo binfotypes.BuildInfo outline outlineCapture epoch *time.Time + scanStage bool + scanContext bool } type dispatchStates struct { @@ -1363,7 +1411,9 @@ func dispatchArg(d *dispatchState, c *instructions.ArgCommand, metaArgs []instru ai := argInfo{definition: arg, location: c.Location()} if buildArg.Value != nil { - d.state = d.state.AddEnv(buildArg.Key, *buildArg.Value) + if _, ok := nonEnvArgs[buildArg.Key]; !ok { + d.state = d.state.AddEnv(buildArg.Key, *buildArg.Value) + } ai.value = *buildArg.Value } @@ -1735,3 +1785,17 @@ func clampTimes(img Image, tm *time.Time) Image { func isHTTPSource(src string) bool { return strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") } + +func isEnabledForStage(stage string, value string) bool { + if enabled, err := strconv.ParseBool(value); err == nil { + return enabled + } + + vv := strings.Split(value, ",") + for _, v := range vv { + if v == stage { + return true + } + } + return false +} diff --git a/frontend/dockerfile/dockerfile2llb/convert_test.go b/frontend/dockerfile/dockerfile2llb/convert_test.go index 8fe1aa6e2ad8..ffcfee86fc15 100644 --- a/frontend/dockerfile/dockerfile2llb/convert_test.go +++ b/frontend/dockerfile/dockerfile2llb/convert_test.go @@ -31,7 +31,7 @@ ENV FOO bar COPY f1 f2 /sub/ RUN ls -l ` - _, _, err := Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) + _, _, _, err := Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) assert.NoError(t, err) df = `FROM scratch AS foo @@ -40,7 +40,7 @@ FROM foo COPY --from=foo f1 / COPY --from=0 f2 / ` - _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) + _, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) assert.NoError(t, err) df = `FROM scratch AS foo @@ -49,12 +49,12 @@ FROM foo COPY --from=foo f1 / COPY --from=0 f2 / ` - _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{ + _, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{ Target: "Foo", }) assert.NoError(t, err) - _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{ + _, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{ Target: "nosuch", }) assert.Error(t, err) @@ -62,21 +62,21 @@ COPY --from=0 f2 / df = `FROM scratch ADD http://github.com/moby/buildkit/blob/master/README.md / ` - _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) + _, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) assert.NoError(t, err) df = `FROM scratch COPY http://github.com/moby/buildkit/blob/master/README.md / ` - _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) + _, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) assert.EqualError(t, err, "source can't be a URL for COPY") df = `FROM "" AS foo` - _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) + _, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) assert.Error(t, err) df = `FROM ${BLANK} AS foo` - _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) + _, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) assert.Error(t, err) } @@ -174,7 +174,7 @@ func TestDockerfileCircularDependencies(t *testing.T) { df := `FROM busybox AS stage0 COPY --from=stage0 f1 /sub/ ` - _, _, err := Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) + _, _, _, err := Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) assert.EqualError(t, err, "circular dependency detected on stage: stage0") // multiple stages with circular dependency @@ -185,6 +185,6 @@ COPY --from=stage0 f2 /sub/ FROM busybox AS stage2 COPY --from=stage1 f2 /sub/ ` - _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) + _, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) assert.EqualError(t, err, "circular dependency detected on stage: stage0") } diff --git a/frontend/dockerfile/dockerfile_test.go b/frontend/dockerfile/dockerfile_test.go index 4fd465ddc55a..5916932c9ac3 100644 --- a/frontend/dockerfile/dockerfile_test.go +++ b/frontend/dockerfile/dockerfile_test.go @@ -153,6 +153,7 @@ var allTests = integration.TestFuncs( testClientFrontendProvenance, testClientLLBProvenance, testSecretSSHProvenance, + testSBOMScannerArgs, ) // Tests that depend on the `security.*` entitlements @@ -6152,6 +6153,7 @@ EOF require.Equal(t, []byte("data\n"), img.Layers[0]["foo"].Data) att := imgs.Find("unknown/unknown") + require.Equal(t, 1, len(att.LayersRaw)) var attest intoto.Statement require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) require.Equal(t, "https://in-toto.io/Statement/v0.1", attest.Type) @@ -6159,6 +6161,236 @@ EOF require.Equal(t, map[string]interface{}{"success": true}, attest.Predicate) } +func testSBOMScannerArgs(t *testing.T, sb integration.Sandbox) { + ctx := sb.Context() + + c, err := client.New(ctx, sb.Address()) + require.NoError(t, err) + defer c.Close() + + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + + f := getFrontend(t, sb) + + dockerfile := []byte(` +FROM busybox:latest +COPY <<-"EOF" /scan.sh + set -e + cat < $BUILDKIT_SCAN_DESTINATION/spdx.json + { + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://spdx.dev/Document", + "predicate": {"core": true} + } + BUNDLE + if [ -d "${BUILDKIT_SCAN_SOURCE_EXTRAS:?}" ]; then + for src in "${BUILDKIT_SCAN_SOURCE_EXTRAS}"/*; do + cat < $BUILDKIT_SCAN_DESTINATION/$(basename $src).spdx.json + { + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://spdx.dev/Document", + "predicate": {"extra": true} + } + BUNDLE + done + fi +EOF +CMD sh /scan.sh +`) + + scannerDir, err := integration.Tmpdir( + t, + fstest.CreateFile("Dockerfile", dockerfile, 0600), + ) + require.NoError(t, err) + + scannerTarget := registry + "/buildkit/testsbomscannerargs:latest" + _, err = f.Solve(sb.Context(), c, client.SolveOpt{ + LocalDirs: map[string]string{ + builder.DefaultLocalNameDockerfile: scannerDir, + builder.DefaultLocalNameContext: scannerDir, + }, + Exports: []client.ExportEntry{ + { + Type: client.ExporterImage, + Attrs: map[string]string{ + "name": scannerTarget, + "push": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + // scan an image with no additional sboms + dockerfile = []byte(` +FROM scratch as base +COPY <