policy: add image.provenance input type
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
+8
-10
@@ -158,7 +158,7 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcReq = buildSourceMetaResponse(resp, req)
|
||||
srcReq = buildSourceMetaResponse(resp)
|
||||
continue
|
||||
}
|
||||
break
|
||||
@@ -249,7 +249,7 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcReq = buildSourceMetaResponse(resp, next)
|
||||
srcReq = buildSourceMetaResponse(resp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,10 +293,11 @@ func sourceResolverOpt(req *gwpb.ResolveSourceMetaRequest, platform *ocispecs.Pl
|
||||
}
|
||||
if req.Image != nil {
|
||||
opt.ImageOpt = &sourceresolver.ResolveImageOpt{
|
||||
NoConfig: req.Image.NoConfig,
|
||||
AttestationChain: req.Image.AttestationChain,
|
||||
Platform: platform,
|
||||
ResolveMode: req.ResolveMode,
|
||||
NoConfig: req.Image.NoConfig,
|
||||
AttestationChain: req.Image.AttestationChain,
|
||||
ResolveAttestations: slices.Clone(req.Image.ResolveAttestations),
|
||||
Platform: platform,
|
||||
ResolveMode: req.ResolveMode,
|
||||
}
|
||||
}
|
||||
if req.Git != nil {
|
||||
@@ -307,15 +308,12 @@ func sourceResolverOpt(req *gwpb.ResolveSourceMetaRequest, platform *ocispecs.Pl
|
||||
return opt
|
||||
}
|
||||
|
||||
func buildSourceMetaResponse(resp *sourceresolver.MetaResponse, req *gwpb.ResolveSourceMetaRequest) *gwpb.ResolveSourceMetaResponse {
|
||||
func buildSourceMetaResponse(resp *sourceresolver.MetaResponse) *gwpb.ResolveSourceMetaResponse {
|
||||
out := &gwpb.ResolveSourceMetaResponse{
|
||||
Source: resp.Op,
|
||||
}
|
||||
if resp.Image != nil {
|
||||
chain := toGatewayAttestationChain(resp.Image.AttestationChain)
|
||||
if chain == nil && req != nil && req.Image != nil && req.Image.AttestationChain {
|
||||
chain = &gwpb.AttestationChain{}
|
||||
}
|
||||
out.Image = &gwpb.ResolveSourceImageResponse{
|
||||
Digest: resp.Image.Digest.String(),
|
||||
Config: resp.Image.Config,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSourceResolverOptIncludesResolveAttestations(t *testing.T) {
|
||||
req := &gwpb.ResolveSourceMetaRequest{
|
||||
ResolveMode: "default",
|
||||
Image: &gwpb.ResolveSourceImageRequest{
|
||||
NoConfig: true,
|
||||
ResolveAttestations: []string{"https://slsa.dev/provenance/v0.2"},
|
||||
},
|
||||
}
|
||||
platform := &ocispecs.Platform{OS: "linux", Architecture: "amd64"}
|
||||
|
||||
opt := sourceResolverOpt(req, platform)
|
||||
require.NotNil(t, opt.ImageOpt)
|
||||
require.True(t, opt.ImageOpt.NoConfig)
|
||||
require.Equal(t, []string{"https://slsa.dev/provenance/v0.2"}, opt.ImageOpt.ResolveAttestations)
|
||||
require.Equal(t, "default", opt.ImageOpt.ResolveMode)
|
||||
require.Equal(t, platform, opt.ImageOpt.Platform)
|
||||
}
|
||||
@@ -167,7 +167,7 @@ func (r *policyTestResolver) Resolve(ctx context.Context, source *pb.SourceOp, r
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildSourceMetaResponse(resp, req), nil
|
||||
return buildSourceMetaResponse(resp), nil
|
||||
}
|
||||
|
||||
func (r *policyTestResolver) init(ctx context.Context) error {
|
||||
|
||||
@@ -46,6 +46,47 @@ func TestAddUnknowns(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "image-provenance-enables-resolve-attestations",
|
||||
unknowns: []string{"image.provenance"},
|
||||
initial: &gwpb.ResolveSourceMetaRequest{},
|
||||
expected: &gwpb.ResolveSourceMetaRequest{
|
||||
Image: &gwpb.ResolveSourceImageRequest{
|
||||
NoConfig: true,
|
||||
AttestationChain: true,
|
||||
ResolveAttestations: resolveProvenanceAttestations,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested-image-provenance-enables-resolve-attestations",
|
||||
unknowns: []string{"image.provenance.buildType"},
|
||||
initial: &gwpb.ResolveSourceMetaRequest{},
|
||||
expected: &gwpb.ResolveSourceMetaRequest{
|
||||
Image: &gwpb.ResolveSourceImageRequest{
|
||||
NoConfig: true,
|
||||
AttestationChain: true,
|
||||
ResolveAttestations: resolveProvenanceAttestations,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "image-provenance-on-existing-image-request-preserves-fields",
|
||||
unknowns: []string{"image.provenance.builderID"},
|
||||
initial: &gwpb.ResolveSourceMetaRequest{
|
||||
Image: &gwpb.ResolveSourceImageRequest{
|
||||
NoConfig: false,
|
||||
AttestationChain: true,
|
||||
},
|
||||
},
|
||||
expected: &gwpb.ResolveSourceMetaRequest{
|
||||
Image: &gwpb.ResolveSourceImageRequest{
|
||||
NoConfig: false,
|
||||
AttestationChain: true,
|
||||
ResolveAttestations: resolveProvenanceAttestations,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "image-attestation-on-existing-image-request",
|
||||
unknowns: []string{"image.hasProvenance"},
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
|
||||
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
|
||||
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
||||
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
|
||||
)
|
||||
|
||||
const predicateTypeAnnotation = "in-toto.io/predicate-type"
|
||||
|
||||
var resolveProvenanceAttestations = []string{
|
||||
slsa02.PredicateSLSAProvenance,
|
||||
slsa1.PredicateSLSAProvenance,
|
||||
}
|
||||
|
||||
type inTotoStatement struct {
|
||||
PredicateType string `json:"predicateType"`
|
||||
Predicate json.RawMessage `json:"predicate"`
|
||||
}
|
||||
|
||||
func parseProvenance(ac *gwpb.AttestationChain) (*ImageProvenance, error) {
|
||||
if ac == nil || len(ac.Blobs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Prefer blobs that explicitly declare a provenance predicate type.
|
||||
for _, b := range ac.Blobs {
|
||||
if b == nil || b.Descriptor_ == nil || len(b.Data) == 0 {
|
||||
continue
|
||||
}
|
||||
pt := b.Descriptor_.Annotations[predicateTypeAnnotation]
|
||||
if pt == "" {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(resolveProvenanceAttestations, pt) {
|
||||
continue
|
||||
}
|
||||
prv, err := parseProvenanceBlob(b.Data, pt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if prv != nil {
|
||||
return prv, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func parseProvenanceBlob(dt []byte, pt string) (*ImageProvenance, error) {
|
||||
var stmt inTotoStatement
|
||||
if err := json.Unmarshal(dt, &stmt); err != nil || len(stmt.Predicate) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if stmt.PredicateType != "" && stmt.PredicateType != pt {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch pt {
|
||||
case slsa1.PredicateSLSAProvenance:
|
||||
return parseSLSA1Provenance(stmt.Predicate)
|
||||
case slsa02.PredicateSLSAProvenance:
|
||||
return parseSLSA02Provenance(stmt.Predicate)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func parseSLSA1Provenance(dt []byte) (*ImageProvenance, error) {
|
||||
var pred provenancetypes.ProvenancePredicateSLSA1
|
||||
if err := json.Unmarshal(dt, &pred); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
if pred.BuildDefinition.BuildType == "" && pred.RunDetails.Builder.ID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
prv := &ImageProvenance{
|
||||
PredicateType: slsa1.PredicateSLSAProvenance,
|
||||
BuildType: pred.BuildDefinition.BuildType,
|
||||
BuilderID: pred.RunDetails.Builder.ID,
|
||||
ConfigSource: &ImageProvenanceConfigSource{
|
||||
URI: pred.BuildDefinition.ExternalParameters.ConfigSource.URI,
|
||||
Digest: pred.BuildDefinition.ExternalParameters.ConfigSource.Digest,
|
||||
Path: pred.BuildDefinition.ExternalParameters.ConfigSource.Path,
|
||||
},
|
||||
Frontend: pred.BuildDefinition.ExternalParameters.Request.Frontend,
|
||||
BuildArgs: extractBuildArgs(pred.BuildDefinition.ExternalParameters.Request.Args),
|
||||
RawArgs: pred.BuildDefinition.ExternalParameters.Request.Args,
|
||||
}
|
||||
|
||||
if md := pred.RunDetails.Metadata; md != nil {
|
||||
prv.InvocationID = md.InvocationID
|
||||
prv.StartedOn = formatProvenanceTime(md.StartedOn)
|
||||
prv.FinishedOn = formatProvenanceTime(md.FinishedOn)
|
||||
prv.Reproducible = boolPtr(md.Reproducible)
|
||||
prv.Hermetic = boolPtr(md.Hermetic)
|
||||
prv.Completeness = &ImageProvenanceCompleteness{
|
||||
Parameters: boolPtr(md.Completeness.Request),
|
||||
Materials: boolPtr(md.Completeness.ResolvedDependencies),
|
||||
}
|
||||
}
|
||||
|
||||
return prv, nil
|
||||
}
|
||||
|
||||
func parseSLSA02Provenance(dt []byte) (*ImageProvenance, error) {
|
||||
var pred provenancetypes.ProvenancePredicateSLSA02
|
||||
if err := json.Unmarshal(dt, &pred); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
if pred.BuildType == "" && pred.Builder.ID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
prv := &ImageProvenance{
|
||||
PredicateType: slsa02.PredicateSLSAProvenance,
|
||||
BuildType: pred.BuildType,
|
||||
BuilderID: pred.Builder.ID,
|
||||
ConfigSource: &ImageProvenanceConfigSource{
|
||||
URI: pred.Invocation.ConfigSource.URI,
|
||||
Digest: pred.Invocation.ConfigSource.Digest,
|
||||
Path: pred.Invocation.ConfigSource.EntryPoint,
|
||||
},
|
||||
Frontend: pred.Invocation.Parameters.Frontend,
|
||||
BuildArgs: extractBuildArgs(pred.Invocation.Parameters.Args),
|
||||
RawArgs: pred.Invocation.Parameters.Args,
|
||||
}
|
||||
|
||||
if md := pred.Metadata; md != nil {
|
||||
prv.InvocationID = md.BuildInvocationID
|
||||
prv.StartedOn = formatProvenanceTime(md.BuildStartedOn)
|
||||
prv.FinishedOn = formatProvenanceTime(md.BuildFinishedOn)
|
||||
prv.Reproducible = boolPtr(md.Reproducible)
|
||||
prv.Hermetic = boolPtr(md.Hermetic)
|
||||
prv.Completeness = &ImageProvenanceCompleteness{
|
||||
Parameters: boolPtr(md.Completeness.Parameters),
|
||||
Environment: boolPtr(md.Completeness.Environment),
|
||||
Materials: boolPtr(md.Completeness.Materials),
|
||||
}
|
||||
}
|
||||
|
||||
return prv, nil
|
||||
}
|
||||
|
||||
func boolPtr(v bool) *bool {
|
||||
return &v
|
||||
}
|
||||
|
||||
func formatProvenanceTime(t *time.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
return t.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func extractBuildArgs(args map[string]string) map[string]string {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
const prefix = "build-arg:"
|
||||
out := make(map[string]string)
|
||||
for k, v := range args {
|
||||
if name, ok := strings.CutPrefix(k, prefix); ok && name != "" {
|
||||
out[name] = v
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -128,9 +128,41 @@ type Image struct {
|
||||
WorkingDir string `json:"workingDir,omitempty"`
|
||||
|
||||
HasProvenance bool `json:"hasProvenance,omitempty"`
|
||||
Provenance *ImageProvenance `json:"provenance,omitempty"`
|
||||
Signatures []AttestationSignature `json:"signatures,omitempty"`
|
||||
}
|
||||
|
||||
type ImageProvenance struct {
|
||||
PredicateType string `json:"predicateType,omitempty"`
|
||||
BuildType string `json:"buildType,omitempty"`
|
||||
BuilderID string `json:"builderID,omitempty"`
|
||||
|
||||
InvocationID string `json:"invocationID,omitempty"`
|
||||
StartedOn string `json:"startedOn,omitempty"`
|
||||
FinishedOn string `json:"finishedOn,omitempty"`
|
||||
|
||||
ConfigSource *ImageProvenanceConfigSource `json:"configSource,omitempty"`
|
||||
Frontend string `json:"frontend,omitempty"`
|
||||
BuildArgs map[string]string `json:"buildArgs,omitempty"`
|
||||
RawArgs map[string]string `json:"rawArgs,omitempty"`
|
||||
Reproducible *bool `json:"reproducible,omitempty"`
|
||||
Hermetic *bool `json:"hermetic,omitempty"`
|
||||
|
||||
Completeness *ImageProvenanceCompleteness `json:"completeness,omitempty"`
|
||||
}
|
||||
|
||||
type ImageProvenanceConfigSource struct {
|
||||
URI string `json:"uri,omitempty"`
|
||||
Digest map[string]string `json:"digest,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
type ImageProvenanceCompleteness struct {
|
||||
Parameters *bool `json:"parameters,omitempty"`
|
||||
Environment *bool `json:"environment,omitempty"`
|
||||
Materials *bool `json:"materials,omitempty"`
|
||||
}
|
||||
|
||||
type AttestationSignature struct {
|
||||
SignatureKind SignatureKind `json:"kind,omitempty"`
|
||||
SignatureType SignatureType `json:"type,omitempty"`
|
||||
|
||||
+33
-3
@@ -555,7 +555,7 @@ func SourceToInputWithLogger(ctx context.Context, getVerifier PolicyVerifierProv
|
||||
unknowns = append(unknowns, "input.image.checksum")
|
||||
}
|
||||
unknowns = append(unknowns, withPrefix(configFields, "input.image.")...)
|
||||
unknowns = append(unknowns, "input.image.hasProvenance", "input.image.signatures")
|
||||
unknowns = append(unknowns, "input.image.hasProvenance", "input.image.provenance", "input.image.signatures")
|
||||
} else {
|
||||
inp.Image.Checksum = src.Image.Digest
|
||||
if cfg := src.Image.Config; cfg != nil {
|
||||
@@ -577,7 +577,14 @@ func SourceToInputWithLogger(ctx context.Context, getVerifier PolicyVerifierProv
|
||||
}
|
||||
|
||||
if ac := src.Image.AttestationChain; ac != nil {
|
||||
inp.Image.HasProvenance = ac.AttestationManifest != ""
|
||||
if prv, err := parseProvenance(ac); err != nil {
|
||||
if logf != nil {
|
||||
logf(logrus.DebugLevel, fmt.Sprintf("failed to parse image provenance: %v", err))
|
||||
}
|
||||
} else {
|
||||
inp.Image.Provenance = prv
|
||||
}
|
||||
inp.Image.HasProvenance = ac.AttestationManifest != "" || inp.Image.Provenance != nil
|
||||
if getVerifier != nil {
|
||||
signatures, err := parseSignatures(ctx, getVerifier, ac, platform)
|
||||
if err != nil {
|
||||
@@ -589,7 +596,7 @@ func SourceToInputWithLogger(ctx context.Context, getVerifier PolicyVerifierProv
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unknowns = append(unknowns, "input.image.hasProvenance", "input.image.signatures")
|
||||
unknowns = append(unknowns, "input.image.hasProvenance", "input.image.provenance", "input.image.signatures")
|
||||
}
|
||||
}
|
||||
case "local":
|
||||
@@ -635,6 +642,17 @@ func AddUnknownsWithLogger(logf func(logrus.Level, string), req *gwpb.ResolveSou
|
||||
logf(logrus.DebugLevel, fmt.Sprintf("collected unknowns: %+v", unk2))
|
||||
}
|
||||
for _, u := range unk2 {
|
||||
if u == "image.provenance" || strings.HasPrefix(u, "image.provenance.") {
|
||||
if req.Image == nil {
|
||||
req.Image = &gwpb.ResolveSourceImageRequest{
|
||||
NoConfig: true,
|
||||
}
|
||||
}
|
||||
req.Image.AttestationChain = true
|
||||
req.Image.ResolveAttestations = appendUnique(req.Image.ResolveAttestations, resolveProvenanceAttestations...)
|
||||
continue
|
||||
}
|
||||
|
||||
switch u {
|
||||
case "image.checksum", "image.labels", "image.user", "image.volumes", "image.workingDir", "image.env":
|
||||
if req.Image == nil {
|
||||
@@ -729,6 +747,9 @@ func summarizeUnknownsForLog(unk []string) []string {
|
||||
if strings.HasPrefix(u, "image.signatures") {
|
||||
u = "image.signatures"
|
||||
}
|
||||
if strings.HasPrefix(u, "image.provenance") {
|
||||
u = "image.provenance"
|
||||
}
|
||||
if u == "image" {
|
||||
continue
|
||||
}
|
||||
@@ -741,6 +762,15 @@ func summarizeUnknownsForLog(unk []string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func appendUnique(dst []string, values ...string) []string {
|
||||
for _, v := range values {
|
||||
if !slices.Contains(dst, v) {
|
||||
dst = append(dst, v)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func hasHTTPUnknowns(unk []string) bool {
|
||||
for _, u := range unk {
|
||||
if strings.HasPrefix(u, "http.") {
|
||||
|
||||
+239
-1
@@ -9,6 +9,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
|
||||
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
|
||||
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
||||
"github.com/moby/buildkit/solver/pb"
|
||||
policyimage "github.com/moby/policy-helpers/image"
|
||||
@@ -177,6 +179,7 @@ func TestSourceToInputWithLogger(t *testing.T) {
|
||||
"input.image.workingDir",
|
||||
"input.image.env",
|
||||
"input.image.hasProvenance",
|
||||
"input.image.provenance",
|
||||
"input.image.signatures",
|
||||
},
|
||||
},
|
||||
@@ -208,6 +211,7 @@ func TestSourceToInputWithLogger(t *testing.T) {
|
||||
"input.image.workingDir",
|
||||
"input.image.env",
|
||||
"input.image.hasProvenance",
|
||||
"input.image.provenance",
|
||||
"input.image.signatures",
|
||||
},
|
||||
},
|
||||
@@ -343,6 +347,113 @@ func TestSourceToInputWithLogger(t *testing.T) {
|
||||
require.Equal(t, "docker/buildx", sig.Signer.SourceRepositoryIdentifier)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "image-attestation-chain-loads-provenance-fields-v0.2",
|
||||
src: &gwpb.ResolveSourceMetaResponse{
|
||||
Source: &pb.SourceOp{
|
||||
Identifier: "docker-image://alpine:latest",
|
||||
},
|
||||
Image: &gwpb.ResolveSourceImageResponse{
|
||||
Digest: "sha256:efefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef",
|
||||
AttestationChain: newTestAttestationChainWithProvenance(t),
|
||||
},
|
||||
},
|
||||
platform: &ocispecs.Platform{OS: "linux", Architecture: "amd64"},
|
||||
assert: func(t *testing.T, inp Input, unknowns []string, err error) {
|
||||
t.Helper()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{
|
||||
"input.image.labels",
|
||||
"input.image.user",
|
||||
"input.image.volumes",
|
||||
"input.image.workingDir",
|
||||
"input.image.env",
|
||||
}, unknowns)
|
||||
require.NotNil(t, inp.Image)
|
||||
require.True(t, inp.Image.HasProvenance)
|
||||
require.NotNil(t, inp.Image.Provenance)
|
||||
require.Equal(t, slsa02.PredicateSLSAProvenance, inp.Image.Provenance.PredicateType)
|
||||
require.Equal(t, "https://example.com/build-type", inp.Image.Provenance.BuildType)
|
||||
require.Equal(t, "https://example.com/builder-id", inp.Image.Provenance.BuilderID)
|
||||
require.Equal(t, "inv-v02", inp.Image.Provenance.InvocationID)
|
||||
require.Equal(t, "2024-01-02T03:04:05Z", inp.Image.Provenance.StartedOn)
|
||||
require.Equal(t, "2024-01-02T03:05:05Z", inp.Image.Provenance.FinishedOn)
|
||||
require.Equal(t, "gateway.v0", inp.Image.Provenance.Frontend)
|
||||
require.Equal(t, map[string]string{"BUILDKIT_CONTEXT_KEEP_GIT_DIR": "1"}, inp.Image.Provenance.BuildArgs)
|
||||
require.Equal(t, map[string]string{
|
||||
"build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR": "1",
|
||||
"cmdline": "docker/dockerfile-upstream:master",
|
||||
}, inp.Image.Provenance.RawArgs)
|
||||
require.NotNil(t, inp.Image.Provenance.ConfigSource)
|
||||
require.Equal(t, "https://github.com/moby/buildkit.git#refs/tags/v0.21.0", inp.Image.Provenance.ConfigSource.URI)
|
||||
require.Equal(t, "Dockerfile", inp.Image.Provenance.ConfigSource.Path)
|
||||
require.Equal(t, map[string]string{"sha1": "52b004d2afe20c5c80967cc1784e718b52d69dae"}, inp.Image.Provenance.ConfigSource.Digest)
|
||||
require.NotNil(t, inp.Image.Provenance.Completeness)
|
||||
require.NotNil(t, inp.Image.Provenance.Completeness.Parameters)
|
||||
require.True(t, *inp.Image.Provenance.Completeness.Parameters)
|
||||
require.NotNil(t, inp.Image.Provenance.Completeness.Environment)
|
||||
require.True(t, *inp.Image.Provenance.Completeness.Environment)
|
||||
require.NotNil(t, inp.Image.Provenance.Completeness.Materials)
|
||||
require.False(t, *inp.Image.Provenance.Completeness.Materials)
|
||||
require.NotNil(t, inp.Image.Provenance.Reproducible)
|
||||
require.True(t, *inp.Image.Provenance.Reproducible)
|
||||
require.NotNil(t, inp.Image.Provenance.Hermetic)
|
||||
require.True(t, *inp.Image.Provenance.Hermetic)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "image-attestation-chain-loads-provenance-fields-v1",
|
||||
src: &gwpb.ResolveSourceMetaResponse{
|
||||
Source: &pb.SourceOp{
|
||||
Identifier: "docker-image://alpine:latest",
|
||||
},
|
||||
Image: &gwpb.ResolveSourceImageResponse{
|
||||
Digest: "sha256:f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0",
|
||||
AttestationChain: newTestAttestationChainWithProvenanceV1(t),
|
||||
},
|
||||
},
|
||||
platform: &ocispecs.Platform{OS: "linux", Architecture: "amd64"},
|
||||
assert: func(t *testing.T, inp Input, unknowns []string, err error) {
|
||||
t.Helper()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{
|
||||
"input.image.labels",
|
||||
"input.image.user",
|
||||
"input.image.volumes",
|
||||
"input.image.workingDir",
|
||||
"input.image.env",
|
||||
}, unknowns)
|
||||
require.NotNil(t, inp.Image)
|
||||
require.True(t, inp.Image.HasProvenance)
|
||||
require.NotNil(t, inp.Image.Provenance)
|
||||
require.Equal(t, slsa1.PredicateSLSAProvenance, inp.Image.Provenance.PredicateType)
|
||||
require.Equal(t, "https://example.com/build-type-v1", inp.Image.Provenance.BuildType)
|
||||
require.Equal(t, "https://example.com/builder-id-v1", inp.Image.Provenance.BuilderID)
|
||||
require.Equal(t, "inv-v1", inp.Image.Provenance.InvocationID)
|
||||
require.Equal(t, "2024-02-03T04:05:06Z", inp.Image.Provenance.StartedOn)
|
||||
require.Equal(t, "2024-02-03T04:06:06Z", inp.Image.Provenance.FinishedOn)
|
||||
require.Equal(t, "gateway.v0", inp.Image.Provenance.Frontend)
|
||||
require.Equal(t, map[string]string{"BUILDKIT_CONTEXT_KEEP_GIT_DIR": "1"}, inp.Image.Provenance.BuildArgs)
|
||||
require.Equal(t, map[string]string{
|
||||
"build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR": "1",
|
||||
"source": "docker/dockerfile-upstream:master",
|
||||
}, inp.Image.Provenance.RawArgs)
|
||||
require.NotNil(t, inp.Image.Provenance.ConfigSource)
|
||||
require.Equal(t, "https://github.com/moby/buildkit.git#refs/heads/master", inp.Image.Provenance.ConfigSource.URI)
|
||||
require.Equal(t, "Dockerfile", inp.Image.Provenance.ConfigSource.Path)
|
||||
require.Equal(t, map[string]string{"sha1": "9836771d0c5b21cbc7f0c38b81be39c42fc46b7b"}, inp.Image.Provenance.ConfigSource.Digest)
|
||||
require.NotNil(t, inp.Image.Provenance.Completeness)
|
||||
require.NotNil(t, inp.Image.Provenance.Completeness.Parameters)
|
||||
require.True(t, *inp.Image.Provenance.Completeness.Parameters)
|
||||
require.Nil(t, inp.Image.Provenance.Completeness.Environment)
|
||||
require.NotNil(t, inp.Image.Provenance.Completeness.Materials)
|
||||
require.False(t, *inp.Image.Provenance.Completeness.Materials)
|
||||
require.NotNil(t, inp.Image.Provenance.Reproducible)
|
||||
require.True(t, *inp.Image.Provenance.Reproducible)
|
||||
require.NotNil(t, inp.Image.Provenance.Hermetic)
|
||||
require.True(t, *inp.Image.Provenance.Hermetic)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "image-attestation-chain-without-manifest-keeps-has-provenance-false",
|
||||
src: &gwpb.ResolveSourceMetaResponse{
|
||||
@@ -428,7 +539,7 @@ func TestSourceToInputWithLogger(t *testing.T) {
|
||||
WorkingDir: "/work",
|
||||
},
|
||||
},
|
||||
expUnk: []string{"input.image.hasProvenance", "input.image.signatures"},
|
||||
expUnk: []string{"input.image.hasProvenance", "input.image.provenance", "input.image.signatures"},
|
||||
},
|
||||
{
|
||||
name: "git-source-missing-full-remote-url-attr",
|
||||
@@ -855,6 +966,133 @@ func newTestAttestationChain(t *testing.T) *gwpb.AttestationChain {
|
||||
}
|
||||
}
|
||||
|
||||
func newTestAttestationChainWithProvenance(t *testing.T) *gwpb.AttestationChain {
|
||||
t.Helper()
|
||||
|
||||
ac := newTestAttestationChain(t)
|
||||
provenancePredicate := map[string]any{
|
||||
"builder": map[string]any{
|
||||
"id": "https://example.com/builder-id",
|
||||
},
|
||||
"buildType": "https://example.com/build-type",
|
||||
"invocation": map[string]any{
|
||||
"configSource": map[string]any{
|
||||
"digest": map[string]any{
|
||||
"sha1": "52b004d2afe20c5c80967cc1784e718b52d69dae",
|
||||
},
|
||||
"entryPoint": "Dockerfile",
|
||||
"uri": "https://github.com/moby/buildkit.git#refs/tags/v0.21.0",
|
||||
},
|
||||
"parameters": map[string]any{
|
||||
"frontend": "gateway.v0",
|
||||
"args": map[string]any{
|
||||
"build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR": "1",
|
||||
"cmdline": "docker/dockerfile-upstream:master",
|
||||
},
|
||||
},
|
||||
"environment": map[string]any{
|
||||
"platform": "linux/amd64",
|
||||
},
|
||||
},
|
||||
"metadata": map[string]any{
|
||||
"buildInvocationID": "inv-v02",
|
||||
"buildStartedOn": "2024-01-02T03:04:05Z",
|
||||
"buildFinishedOn": "2024-01-02T03:05:05Z",
|
||||
"completeness": map[string]any{
|
||||
"parameters": true,
|
||||
"environment": true,
|
||||
"materials": false,
|
||||
},
|
||||
"reproducible": true,
|
||||
"https://mobyproject.org/buildkit@v1#hermetic": true,
|
||||
},
|
||||
}
|
||||
provenanceBytes := mustMarshalJSON(t, map[string]any{
|
||||
"_type": "https://in-toto.io/Statement/v0.1",
|
||||
"predicateType": slsa02.PredicateSLSAProvenance,
|
||||
"predicate": provenancePredicate,
|
||||
})
|
||||
provenanceDigest := digest.FromBytes(provenanceBytes)
|
||||
|
||||
ac.Blobs[provenanceDigest.String()] = &gwpb.Blob{
|
||||
Descriptor_: &gwpb.Descriptor{
|
||||
MediaType: "application/vnd.in-toto+json",
|
||||
Digest: provenanceDigest.String(),
|
||||
Size: int64(len(provenanceBytes)),
|
||||
Annotations: map[string]string{
|
||||
predicateTypeAnnotation: slsa02.PredicateSLSAProvenance,
|
||||
},
|
||||
},
|
||||
Data: provenanceBytes,
|
||||
}
|
||||
|
||||
return ac
|
||||
}
|
||||
|
||||
func newTestAttestationChainWithProvenanceV1(t *testing.T) *gwpb.AttestationChain {
|
||||
t.Helper()
|
||||
|
||||
ac := newTestAttestationChain(t)
|
||||
provenancePredicate := map[string]any{
|
||||
"buildDefinition": map[string]any{
|
||||
"buildType": "https://example.com/build-type-v1",
|
||||
"externalParameters": map[string]any{
|
||||
"configSource": map[string]any{
|
||||
"digest": map[string]any{
|
||||
"sha1": "9836771d0c5b21cbc7f0c38b81be39c42fc46b7b",
|
||||
},
|
||||
"path": "Dockerfile",
|
||||
"uri": "https://github.com/moby/buildkit.git#refs/heads/master",
|
||||
},
|
||||
"request": map[string]any{
|
||||
"frontend": "gateway.v0",
|
||||
"args": map[string]any{
|
||||
"build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR": "1",
|
||||
"source": "docker/dockerfile-upstream:master",
|
||||
},
|
||||
},
|
||||
},
|
||||
"internalParameters": map[string]any{},
|
||||
},
|
||||
"runDetails": map[string]any{
|
||||
"builder": map[string]any{
|
||||
"id": "https://example.com/builder-id-v1",
|
||||
},
|
||||
"metadata": map[string]any{
|
||||
"invocationID": "inv-v1",
|
||||
"startedOn": "2024-02-03T04:05:06Z",
|
||||
"finishedOn": "2024-02-03T04:06:06Z",
|
||||
"buildkit_completeness": map[string]any{
|
||||
"request": true,
|
||||
"resolvedDependencies": false,
|
||||
},
|
||||
"buildkit_reproducible": true,
|
||||
"buildkit_hermetic": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
provenanceBytes := mustMarshalJSON(t, map[string]any{
|
||||
"_type": "https://in-toto.io/Statement/v0.1",
|
||||
"predicateType": slsa1.PredicateSLSAProvenance,
|
||||
"predicate": provenancePredicate,
|
||||
})
|
||||
provenanceDigest := digest.FromBytes(provenanceBytes)
|
||||
|
||||
ac.Blobs[provenanceDigest.String()] = &gwpb.Blob{
|
||||
Descriptor_: &gwpb.Descriptor{
|
||||
MediaType: "application/vnd.in-toto+json",
|
||||
Digest: provenanceDigest.String(),
|
||||
Size: int64(len(provenanceBytes)),
|
||||
Annotations: map[string]string{
|
||||
predicateTypeAnnotation: slsa1.PredicateSLSAProvenance,
|
||||
},
|
||||
},
|
||||
Data: provenanceBytes,
|
||||
}
|
||||
|
||||
return ac
|
||||
}
|
||||
|
||||
func mustMarshalJSON(t *testing.T, v any) []byte {
|
||||
t.Helper()
|
||||
dt, err := json.Marshal(v)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
urlpkg "net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/continuity/fs/fstest"
|
||||
@@ -23,6 +24,8 @@ var policyEvalTests = []func(t *testing.T, sb integration.Sandbox){
|
||||
testPolicyEvalPrint,
|
||||
testPolicyEvalFields,
|
||||
testPolicyEvalLabel,
|
||||
testPolicyEvalProvenance,
|
||||
testPolicyEvalProvenanceValidation,
|
||||
testPolicyEvalHTTP,
|
||||
}
|
||||
|
||||
@@ -220,6 +223,155 @@ decision := {"allow": allow}
|
||||
require.NoError(t, err, string(out))
|
||||
}
|
||||
|
||||
func testPolicyEvalProvenance(t *testing.T, sb integration.Sandbox) {
|
||||
// TODO: update after v0.28+ to test with non-master BuildKit
|
||||
if buildkitTag() != "master" {
|
||||
t.Skip("policy eval provenance integration requires TEST_BUILDKIT_TAG=master")
|
||||
}
|
||||
if !isDockerContainerWorker(sb) {
|
||||
t.Skip("policy eval provenance integration requires docker-container worker")
|
||||
}
|
||||
// Base policy input support.
|
||||
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
|
||||
|
||||
imageRef := createPolicyEvalProvenanceImage(t, sb)
|
||||
|
||||
cmd := buildxCmd(sb, withArgs(
|
||||
"policy",
|
||||
"eval",
|
||||
"--print",
|
||||
"--fields",
|
||||
"image.provenance",
|
||||
"docker-image://"+imageRef,
|
||||
))
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
out, err := cmd.Output()
|
||||
expectResolveAttestations := true
|
||||
t.Logf("buildkit image=%s worker=%s expectResolveAttestations=%v", buildkitImage, sb.Name(), expectResolveAttestations)
|
||||
if err != nil && strings.Contains(stderr.String(), "maximum attempts reached for resolving policy metadata") {
|
||||
if expectResolveAttestations {
|
||||
require.NoError(t, err, stderr.String())
|
||||
}
|
||||
t.Skip("policy eval provenance requires BuildKit ResolveAttestations support (currently master-only)")
|
||||
}
|
||||
require.NoError(t, err, stderr.String())
|
||||
|
||||
var input policy.Input
|
||||
err = json.Unmarshal(out, &input)
|
||||
require.NoError(t, err, string(out))
|
||||
require.NotNil(t, input.Image)
|
||||
require.True(t, input.Image.HasProvenance, "expected source image to report provenance")
|
||||
if input.Image.Provenance == nil {
|
||||
if expectResolveAttestations {
|
||||
require.NotNil(t, input.Image.Provenance, "expected image provenance to be resolved with master BuildKit")
|
||||
}
|
||||
t.Skip("policy eval provenance requires BuildKit ResolveAttestations support (currently master-only)")
|
||||
}
|
||||
require.Equal(t, "https://slsa.dev/provenance/v1", input.Image.Provenance.PredicateType)
|
||||
require.Equal(t, "dockerfile.v0", input.Image.Provenance.Frontend)
|
||||
}
|
||||
|
||||
func testPolicyEvalProvenanceValidation(t *testing.T, sb integration.Sandbox) {
|
||||
// TODO: update after v0.28+ is released to test with non-master BuildKit
|
||||
if buildkitTag() != "master" {
|
||||
t.Skip("policy eval provenance integration requires TEST_BUILDKIT_TAG=master")
|
||||
}
|
||||
if !isDockerContainerWorker(sb) {
|
||||
t.Skip("policy eval provenance integration requires docker-container worker")
|
||||
}
|
||||
// Base policy input support.
|
||||
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
|
||||
|
||||
imageRef := createPolicyEvalProvenanceImage(t, sb)
|
||||
|
||||
allowPolicyDir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("policy.rego", []byte(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if {
|
||||
input.image.hasProvenance
|
||||
input.image.provenance.predicateType == "https://slsa.dev/provenance/v1"
|
||||
input.image.provenance.frontend == "dockerfile.v0"
|
||||
}
|
||||
|
||||
decision := {"allow": allow}
|
||||
`), 0600),
|
||||
)
|
||||
|
||||
allowCmd := buildxCmd(sb, withDir(allowPolicyDir), withArgs(
|
||||
"policy",
|
||||
"eval",
|
||||
"--filename",
|
||||
"policy",
|
||||
"docker-image://"+imageRef,
|
||||
))
|
||||
allowOut, err := allowCmd.CombinedOutput()
|
||||
if err != nil && strings.Contains(string(allowOut), "maximum attempts reached for resolving policy metadata") {
|
||||
t.Skip("policy eval provenance requires BuildKit ResolveAttestations support (currently master-only)")
|
||||
}
|
||||
require.NoError(t, err, string(allowOut))
|
||||
|
||||
denyPolicyDir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("policy.rego", []byte(`
|
||||
package docker
|
||||
|
||||
default allow = false
|
||||
|
||||
allow if input.image.provenance.frontend == "not-a-real-frontend"
|
||||
|
||||
decision := {"allow": allow}
|
||||
`), 0600),
|
||||
)
|
||||
|
||||
denyCmd := buildxCmd(sb, withDir(denyPolicyDir), withArgs(
|
||||
"policy",
|
||||
"eval",
|
||||
"--filename",
|
||||
"policy",
|
||||
"docker-image://"+imageRef,
|
||||
))
|
||||
denyOut, err := denyCmd.CombinedOutput()
|
||||
if err != nil && strings.Contains(string(denyOut), "maximum attempts reached for resolving policy metadata") {
|
||||
t.Skip("policy eval provenance requires BuildKit ResolveAttestations support (currently master-only)")
|
||||
}
|
||||
require.Error(t, err, string(denyOut))
|
||||
require.Contains(t, string(denyOut), "policy denied")
|
||||
}
|
||||
|
||||
func createPolicyEvalProvenanceImage(t *testing.T, sb integration.Sandbox) string {
|
||||
registry, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
imageRef := registry + "/buildx/policy-eval-provenance:" + identity.NewID()
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile", []byte(`
|
||||
FROM busybox:latest
|
||||
RUN echo provenance > /proof
|
||||
`), 0600),
|
||||
)
|
||||
|
||||
buildCmd := buildxCmd(sb, withDir(dir), withArgs(
|
||||
"build",
|
||||
"--progress=plain",
|
||||
"--provenance=mode=max,version=v1",
|
||||
"--output=type=image,name="+imageRef+",push=true",
|
||||
dir,
|
||||
))
|
||||
buildOut, err := buildCmd.CombinedOutput()
|
||||
require.NoError(t, err, string(buildOut))
|
||||
|
||||
return imageRef
|
||||
}
|
||||
|
||||
func testPolicyEvalHTTP(t *testing.T, sb integration.Sandbox) {
|
||||
resp := &httpserver.Response{Content: []byte("policy-eval-http")}
|
||||
server := httpserver.NewTestServer(map[string]*httpserver.Response{
|
||||
|
||||
Reference in New Issue
Block a user