policy: simplify recursive material resolution
Unify root/material unknown resolution with recursive Input traversal. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
@@ -109,6 +109,9 @@ linters:
|
|||||||
- linters:
|
- linters:
|
||||||
- revive
|
- revive
|
||||||
text: if-return
|
text: if-return
|
||||||
|
- linters:
|
||||||
|
- staticcheck
|
||||||
|
text: "SA1019: .* is deprecated: .*in-toto/attestation"
|
||||||
paths:
|
paths:
|
||||||
- .*\.pb\.go$
|
- .*\.pb\.go$
|
||||||
|
|
||||||
|
|||||||
+158
-109
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -17,7 +18,6 @@ import (
|
|||||||
"github.com/docker/buildx/util/confutil"
|
"github.com/docker/buildx/util/confutil"
|
||||||
"github.com/docker/buildx/util/sourcemeta"
|
"github.com/docker/buildx/util/sourcemeta"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/moby/buildkit/client/llb/sourceresolver"
|
|
||||||
"github.com/moby/buildkit/frontend/dockerui"
|
"github.com/moby/buildkit/frontend/dockerui"
|
||||||
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
||||||
"github.com/moby/buildkit/solver/pb"
|
"github.com/moby/buildkit/solver/pb"
|
||||||
@@ -27,7 +27,6 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type evalOpts struct {
|
type evalOpts struct {
|
||||||
@@ -111,60 +110,66 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva
|
|||||||
srcReq := &gwpb.ResolveSourceMetaResponse{
|
srcReq := &gwpb.ResolveSourceMetaResponse{
|
||||||
Source: src,
|
Source: src,
|
||||||
}
|
}
|
||||||
|
input, err := policy.SourceToInput(ctx, verifier, srcReq, &p, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
maxAttempts := 5
|
maxAttempts := 5
|
||||||
var unknowns []string
|
|
||||||
var lastUnknowns []string
|
var lastUnknowns []string
|
||||||
var trimmedUnknowns []string
|
var trimmedUnknowns []string
|
||||||
var input policy.Input
|
|
||||||
var doneInvalidCheck bool
|
|
||||||
var invalidFields []string
|
var invalidFields []string
|
||||||
for {
|
for {
|
||||||
maxAttempts--
|
maxAttempts--
|
||||||
if maxAttempts <= 0 {
|
if maxAttempts <= 0 {
|
||||||
return errors.New("maximum attempts reached for resolving source metadata")
|
return errors.New("maximum attempts reached for resolving source metadata")
|
||||||
}
|
}
|
||||||
input, unknowns, err = policy.SourceToInput(ctx, verifier, srcReq, &p)
|
unknowns := input.Unknowns()
|
||||||
if err != nil {
|
trimmedUnknowns = make([]string, 0, len(unknowns))
|
||||||
return err
|
for _, u := range unknowns {
|
||||||
|
trimmedUnknowns = append(trimmedUnknowns, strings.TrimPrefix(u, "input."))
|
||||||
}
|
}
|
||||||
trimmedUnknowns = trimInputPrefixSlice(unknowns)
|
|
||||||
if lastUnknowns != nil && slices.Equal(trimmedUnknowns, lastUnknowns) {
|
if lastUnknowns != nil && slices.Equal(trimmedUnknowns, lastUnknowns) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
lastUnknowns = slices.Clone(trimmedUnknowns)
|
lastUnknowns = slices.Clone(trimmedUnknowns)
|
||||||
toReload := []string{}
|
toReload, invalid := selectReloadFields(opts.fields, trimmedUnknowns)
|
||||||
for _, f := range opts.fields {
|
invalidFields = invalid
|
||||||
if slices.Contains(trimmedUnknowns, f) {
|
|
||||||
toReload = append(toReload, f)
|
|
||||||
} else if !doneInvalidCheck {
|
|
||||||
invalidFields = append(invalidFields, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
doneInvalidCheck = true
|
|
||||||
if len(toReload) > 0 {
|
if len(toReload) > 0 {
|
||||||
req := &gwpb.ResolveSourceMetaRequest{}
|
retry, next, err := policy.ResolveInputUnknowns(ctx, &input, srcReq.Source, toReload, platform, &p, metaResolver, verifier, nil)
|
||||||
if err := policy.AddUnknowns(req, toReload); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
opt := sourceResolverOpt(req, &p)
|
if next != nil {
|
||||||
resp, err := metaResolver.ResolveSourceMetadata(ctx, src, opt)
|
resp, err := metaResolver.ResolveSourceMetadata(ctx, next.Source, sourcemeta.ToResolverOpt(next, &p))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srcReq = sourcemeta.ToGatewayMetaResponse(resp)
|
||||||
|
input, err = policy.SourceToInput(ctx, verifier, srcReq, &p, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
srcReq = buildSourceMetaResponse(resp)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if retry {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(invalidFields) > 0 {
|
if len(invalidFields) > 0 {
|
||||||
logrus.Warnf("invalid fields: %v", strings.Join(invalidFields, ", "))
|
logrus.Warnf("invalid fields: %v", strings.Join(invalidFields, ", "))
|
||||||
}
|
}
|
||||||
if len(trimmedUnknowns) > 0 {
|
reportedUnknowns := summarizeEvalUnknowns(trimmedUnknowns, opts.fields)
|
||||||
logrus.Infof("unresolved fields: %v", strings.Join(trimmedUnknowns, ", "))
|
if len(reportedUnknowns) > 0 {
|
||||||
|
logrus.Infof("unresolved fields: %v", strings.Join(reportedUnknowns, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
dt, err := json.MarshalIndent(input, "", " ")
|
printInput := input
|
||||||
|
sanitizePrintInput(&printInput)
|
||||||
|
|
||||||
|
dt, err := json.MarshalIndent(printInput, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to marshal policy input")
|
return errors.Wrap(err, "failed to marshal policy input")
|
||||||
}
|
}
|
||||||
@@ -198,6 +203,9 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva
|
|||||||
env := policy.Env{
|
env := policy.Env{
|
||||||
Filename: filepath.Base(policyName),
|
Filename: filepath.Base(policyName),
|
||||||
}
|
}
|
||||||
|
policyLog := func(_ logrus.Level, msg string) {
|
||||||
|
logrus.Debug(msg)
|
||||||
|
}
|
||||||
|
|
||||||
policyEval := policy.NewPolicy(policy.Opt{
|
policyEval := policy.NewPolicy(policy.Opt{
|
||||||
Files: []policy.File{
|
Files: []policy.File{
|
||||||
@@ -207,9 +215,11 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Env: env,
|
Env: env,
|
||||||
|
Log: policyLog,
|
||||||
FS: fsProvider,
|
FS: fsProvider,
|
||||||
VerifierProvider: verifier,
|
VerifierProvider: verifier,
|
||||||
DefaultPlatform: &p,
|
DefaultPlatform: &p,
|
||||||
|
SourceResolver: metaResolver,
|
||||||
})
|
})
|
||||||
|
|
||||||
srcReq := &gwpb.ResolveSourceMetaResponse{
|
srcReq := &gwpb.ResolveSourceMetaResponse{
|
||||||
@@ -233,114 +243,153 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva
|
|||||||
return evalDecisionError(decision)
|
return evalDecisionError(decision)
|
||||||
}
|
}
|
||||||
|
|
||||||
opt := sourceResolverOpt(next, &p)
|
opt := sourcemeta.ToResolverOpt(next, &p)
|
||||||
resp, err := metaResolver.ResolveSourceMetadata(ctx, src, opt)
|
target := src
|
||||||
|
if next.Source != nil {
|
||||||
|
target = next.Source
|
||||||
|
}
|
||||||
|
resp, err := metaResolver.ResolveSourceMetadata(ctx, target, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
srcReq = buildSourceMetaResponse(resp)
|
srcReq = sourcemeta.ToGatewayMetaResponse(resp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toGatewayDescriptor(desc ocispecs.Descriptor) *gwpb.Descriptor {
|
func selectReloadFields(fields []string, unknowns []string) ([]string, []string) {
|
||||||
return &gwpb.Descriptor{
|
if len(fields) == 0 {
|
||||||
MediaType: desc.MediaType,
|
return nil, nil
|
||||||
Digest: desc.Digest.String(),
|
|
||||||
Size: desc.Size,
|
|
||||||
Annotations: desc.Annotations,
|
|
||||||
}
|
}
|
||||||
|
reload := map[string]struct{}{}
|
||||||
|
var invalid []string
|
||||||
|
for _, field := range fields {
|
||||||
|
if prereq, ok := materialFieldPrerequisites(field); ok {
|
||||||
|
added := false
|
||||||
|
for _, p := range prereq {
|
||||||
|
if slices.Contains(unknowns, p) {
|
||||||
|
reload[p] = struct{}{}
|
||||||
|
added = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if slices.Contains(unknowns, field) {
|
||||||
|
reload[field] = struct{}{}
|
||||||
|
added = true
|
||||||
|
} else if ancestor := findUnknownAncestor(field, unknowns); ancestor != "" {
|
||||||
|
reload[ancestor] = struct{}{}
|
||||||
|
added = true
|
||||||
|
}
|
||||||
|
if !added {
|
||||||
|
invalid = append(invalid, field)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if slices.Contains(unknowns, field) {
|
||||||
|
reload[field] = struct{}{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
invalid = append(invalid, field)
|
||||||
|
}
|
||||||
|
return slices.Collect(maps.Keys(reload)), invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
func toGatewayAttestationChain(chain *sourceresolver.AttestationChain) *gwpb.AttestationChain {
|
func findUnknownAncestor(field string, unknowns []string) string {
|
||||||
if chain == nil {
|
var best string
|
||||||
|
for _, unknown := range unknowns {
|
||||||
|
if field == unknown {
|
||||||
|
return unknown
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(field, unknown+".") {
|
||||||
|
if len(unknown) > len(best) {
|
||||||
|
best = unknown
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(field, unknown+"[") {
|
||||||
|
if len(unknown) > len(best) {
|
||||||
|
best = unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
func materialFieldPrerequisites(field string) ([]string, bool) {
|
||||||
|
const seg = ".image.provenance.materials["
|
||||||
|
if !strings.HasPrefix(field, seg[1:]) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
provenancePath := strings.TrimSuffix(seg, ".materials[")
|
||||||
|
out := map[string]struct{}{strings.TrimPrefix(provenancePath, "."): {}}
|
||||||
|
collectMaterialPrerequisites(field, seg, provenancePath, 0, out)
|
||||||
|
keys := slices.Collect(maps.Keys(out))
|
||||||
|
slices.Sort(keys)
|
||||||
|
return keys, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectMaterialPrerequisites(field, seg, provenancePath string, start int, out map[string]struct{}) {
|
||||||
|
i := strings.Index(field[start:], seg)
|
||||||
|
if i < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i += start
|
||||||
|
out[field[:i]+provenancePath] = struct{}{}
|
||||||
|
collectMaterialPrerequisites(field, seg, provenancePath, i+len(seg), out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func summarizeEvalUnknowns(unknowns, requested []string) []string {
|
||||||
|
if len(unknowns) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
signatures := make([]string, 0, len(chain.SignatureManifests))
|
if len(requested) > 0 {
|
||||||
for _, dgst := range chain.SignatureManifests {
|
out := map[string]struct{}{}
|
||||||
signatures = append(signatures, dgst.String())
|
for _, field := range requested {
|
||||||
|
if slices.Contains(unknowns, field) {
|
||||||
|
out[field] = struct{}{}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
blobs := make(map[string]*gwpb.Blob, len(chain.Blobs))
|
if ancestor := findUnknownAncestor(field, unknowns); ancestor != "" {
|
||||||
for dgst, blob := range chain.Blobs {
|
out[ancestor] = struct{}{}
|
||||||
blobs[dgst.String()] = &gwpb.Blob{
|
|
||||||
Descriptor_: toGatewayDescriptor(blob.Descriptor),
|
|
||||||
Data: blob.Data,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &gwpb.AttestationChain{
|
keys := slices.Collect(maps.Keys(out))
|
||||||
Root: chain.Root.String(),
|
slices.Sort(keys)
|
||||||
ImageManifest: chain.ImageManifest.String(),
|
return keys
|
||||||
AttestationManifest: chain.AttestationManifest.String(),
|
|
||||||
SignatureManifests: signatures,
|
|
||||||
Blobs: blobs,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out := map[string]struct{}{}
|
||||||
|
for _, u := range unknowns {
|
||||||
|
out[summarizeUnknownField(u)] = struct{}{}
|
||||||
|
}
|
||||||
|
keys := slices.Collect(maps.Keys(out))
|
||||||
|
slices.Sort(keys)
|
||||||
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
func sourceResolverOpt(req *gwpb.ResolveSourceMetaRequest, platform *ocispecs.Platform) sourceresolver.Opt {
|
func summarizeUnknownField(field string) string {
|
||||||
opt := sourceresolver.Opt{
|
if base, _, ok := strings.Cut(field, ".materials["); ok {
|
||||||
LogName: req.LogName,
|
return base + ".materials"
|
||||||
SourcePolicies: req.SourcePolicies,
|
|
||||||
}
|
}
|
||||||
if req.Image != nil {
|
if strings.HasPrefix(field, "materials[") {
|
||||||
opt.ImageOpt = &sourceresolver.ResolveImageOpt{
|
return "materials"
|
||||||
NoConfig: req.Image.NoConfig,
|
|
||||||
AttestationChain: req.Image.AttestationChain,
|
|
||||||
ResolveAttestations: slices.Clone(req.Image.ResolveAttestations),
|
|
||||||
Platform: platform,
|
|
||||||
ResolveMode: req.ResolveMode,
|
|
||||||
}
|
}
|
||||||
|
parts := strings.Split(field, ".")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
return strings.Join(parts[:2], ".")
|
||||||
}
|
}
|
||||||
if req.Git != nil {
|
return field
|
||||||
opt.GitOpt = &sourceresolver.ResolveGitOpt{
|
|
||||||
ReturnObject: req.Git.ReturnObject,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return opt
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSourceMetaResponse(resp *sourceresolver.MetaResponse) *gwpb.ResolveSourceMetaResponse {
|
func sanitizePrintInput(inp *policy.Input) {
|
||||||
out := &gwpb.ResolveSourceMetaResponse{
|
if inp == nil {
|
||||||
Source: resp.Op,
|
return
|
||||||
}
|
}
|
||||||
if resp.Image != nil {
|
inp.Env.Depth = 0
|
||||||
chain := toGatewayAttestationChain(resp.Image.AttestationChain)
|
if inp.Image == nil || inp.Image.Provenance == nil || len(inp.Image.Provenance.Materials) == 0 {
|
||||||
out.Image = &gwpb.ResolveSourceImageResponse{
|
return
|
||||||
Digest: resp.Image.Digest.String(),
|
|
||||||
Config: resp.Image.Config,
|
|
||||||
AttestationChain: chain,
|
|
||||||
}
|
}
|
||||||
|
for i := range inp.Image.Provenance.Materials {
|
||||||
|
sanitizePrintInput(&inp.Image.Provenance.Materials[i])
|
||||||
}
|
}
|
||||||
if resp.Git != nil {
|
|
||||||
out.Git = &gwpb.ResolveSourceGitResponse{
|
|
||||||
Checksum: resp.Git.Checksum,
|
|
||||||
Ref: resp.Git.Ref,
|
|
||||||
CommitChecksum: resp.Git.CommitChecksum,
|
|
||||||
CommitObject: resp.Git.CommitObject,
|
|
||||||
TagObject: resp.Git.TagObject,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if resp.HTTP != nil {
|
|
||||||
var lastModified *timestamppb.Timestamp
|
|
||||||
if resp.HTTP.LastModified != nil {
|
|
||||||
lastModified = timestamppb.New(*resp.HTTP.LastModified)
|
|
||||||
}
|
|
||||||
out.HTTP = &gwpb.ResolveSourceHTTPResponse{
|
|
||||||
Checksum: resp.HTTP.Digest.String(),
|
|
||||||
Filename: resp.HTTP.Filename,
|
|
||||||
LastModified: lastModified,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func trimInputPrefixSlice(fields []string) []string {
|
|
||||||
if len(fields) == 0 {
|
|
||||||
return fields
|
|
||||||
}
|
|
||||||
out := make([]string, 0, len(fields))
|
|
||||||
for _, field := range fields {
|
|
||||||
out = append(out, strings.TrimPrefix(field, "input."))
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func evalDecisionError(decision *policysession.DecisionResponse) error {
|
func evalDecisionError(decision *policysession.DecisionResponse) error {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package policy
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
policytypes "github.com/docker/buildx/policy"
|
||||||
|
"github.com/docker/buildx/util/sourcemeta"
|
||||||
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
||||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -18,10 +20,132 @@ func TestSourceResolverOptIncludesResolveAttestations(t *testing.T) {
|
|||||||
}
|
}
|
||||||
platform := &ocispecs.Platform{OS: "linux", Architecture: "amd64"}
|
platform := &ocispecs.Platform{OS: "linux", Architecture: "amd64"}
|
||||||
|
|
||||||
opt := sourceResolverOpt(req, platform)
|
opt := sourcemeta.ToResolverOpt(req, platform)
|
||||||
require.NotNil(t, opt.ImageOpt)
|
require.NotNil(t, opt.ImageOpt)
|
||||||
require.True(t, opt.ImageOpt.NoConfig)
|
require.True(t, opt.ImageOpt.NoConfig)
|
||||||
require.Equal(t, []string{"https://slsa.dev/provenance/v0.2"}, opt.ImageOpt.ResolveAttestations)
|
require.Equal(t, []string{"https://slsa.dev/provenance/v0.2"}, opt.ImageOpt.ResolveAttestations)
|
||||||
require.Equal(t, "default", opt.ImageOpt.ResolveMode)
|
require.Equal(t, "default", opt.ImageOpt.ResolveMode)
|
||||||
require.Equal(t, platform, opt.ImageOpt.Platform)
|
require.Equal(t, platform, opt.ImageOpt.Platform)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSanitizePrintInputClearsDepthRecursively(t *testing.T) {
|
||||||
|
inp := policytypes.Input{
|
||||||
|
Env: policytypes.Env{Depth: 7, Filename: "Dockerfile"},
|
||||||
|
Image: &policytypes.Image{
|
||||||
|
Provenance: &policytypes.ImageProvenance{
|
||||||
|
Materials: []policytypes.Input{
|
||||||
|
{
|
||||||
|
Env: policytypes.Env{Depth: 3, Target: "app"},
|
||||||
|
Image: &policytypes.Image{
|
||||||
|
Provenance: &policytypes.ImageProvenance{
|
||||||
|
Materials: []policytypes.Input{
|
||||||
|
{Env: policytypes.Env{Depth: 2}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitizePrintInput(&inp)
|
||||||
|
|
||||||
|
require.Zero(t, inp.Env.Depth)
|
||||||
|
require.Equal(t, "Dockerfile", inp.Env.Filename)
|
||||||
|
require.Zero(t, inp.Image.Provenance.Materials[0].Env.Depth)
|
||||||
|
require.Equal(t, "app", inp.Image.Provenance.Materials[0].Env.Target)
|
||||||
|
require.Zero(t, inp.Image.Provenance.Materials[0].Image.Provenance.Materials[0].Env.Depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectReloadFields(t *testing.T) {
|
||||||
|
unknowns := []string{
|
||||||
|
"image.provenance",
|
||||||
|
"image.provenance.materials[0].image.hasProvenance",
|
||||||
|
"git.tag",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("exact match", func(t *testing.T) {
|
||||||
|
reload, invalid := selectReloadFields([]string{"git.tag"}, unknowns)
|
||||||
|
require.Equal(t, []string{"git.tag"}, reload)
|
||||||
|
require.Nil(t, invalid)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ancestor mapping", func(t *testing.T) {
|
||||||
|
reload, invalid := selectReloadFields([]string{"image.provenance.materials[0].image.labels"}, unknowns)
|
||||||
|
require.ElementsMatch(t, []string{"image.provenance"}, reload)
|
||||||
|
require.Nil(t, invalid)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("dedupe mapped reloads", func(t *testing.T) {
|
||||||
|
reload, invalid := selectReloadFields([]string{
|
||||||
|
"image.provenance.materials[0].image.labels",
|
||||||
|
"image.provenance.materials[0].image.user",
|
||||||
|
}, unknowns)
|
||||||
|
require.ElementsMatch(t, []string{
|
||||||
|
"image.provenance",
|
||||||
|
}, reload)
|
||||||
|
require.Nil(t, invalid)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid fields reported", func(t *testing.T) {
|
||||||
|
reload, invalid := selectReloadFields([]string{"image.labels", "foo.bar"}, unknowns)
|
||||||
|
require.Empty(t, reload)
|
||||||
|
require.Equal(t, []string{"image.labels", "foo.bar"}, invalid)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("mix exact mapped invalid", func(t *testing.T) {
|
||||||
|
reload, invalid := selectReloadFields([]string{
|
||||||
|
"git.tag",
|
||||||
|
"image.provenance.materials[0].image.env",
|
||||||
|
"no.such.field",
|
||||||
|
}, unknowns)
|
||||||
|
require.ElementsMatch(t, []string{"git.tag", "image.provenance"}, reload)
|
||||||
|
require.Equal(t, []string{"no.such.field"}, invalid)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nested material prerequisites", func(t *testing.T) {
|
||||||
|
reload, invalid := selectReloadFields([]string{
|
||||||
|
"image.provenance.materials[0].image.provenance.materials[1].image.labels",
|
||||||
|
}, unknowns)
|
||||||
|
require.ElementsMatch(t, []string{
|
||||||
|
"image.provenance",
|
||||||
|
}, reload)
|
||||||
|
require.Nil(t, invalid)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("material field after provenance loaded", func(t *testing.T) {
|
||||||
|
reload, invalid := selectReloadFields([]string{
|
||||||
|
"image.provenance.materials[0].image.hasProvenance",
|
||||||
|
}, []string{
|
||||||
|
"image.provenance.materials[0].image.hasProvenance",
|
||||||
|
})
|
||||||
|
require.ElementsMatch(t, []string{
|
||||||
|
"image.provenance.materials[0].image.hasProvenance",
|
||||||
|
}, reload)
|
||||||
|
require.Nil(t, invalid)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaterialFieldPrerequisites(t *testing.T) {
|
||||||
|
t.Run("non material field", func(t *testing.T) {
|
||||||
|
prereq, ok := materialFieldPrerequisites("image.provenance")
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Nil(t, prereq)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("single level material field", func(t *testing.T) {
|
||||||
|
prereq, ok := materialFieldPrerequisites("image.provenance.materials[0].image.labels")
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, []string{"image.provenance"}, prereq)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nested material field", func(t *testing.T) {
|
||||||
|
prereq, ok := materialFieldPrerequisites("image.provenance.materials[0].image.provenance.materials[1].image.labels")
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, []string{
|
||||||
|
"image.provenance",
|
||||||
|
"image.provenance.materials[0].image.provenance",
|
||||||
|
}, prereq)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -158,12 +158,12 @@ func (r *policyTestOptionsProvider) Resolve(ctx context.Context, source *pb.Sour
|
|||||||
if err := r.init(ctx); err != nil {
|
if err := r.init(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
opt := sourceResolverOpt(req, r.platform)
|
opt := sourcemeta.ToResolverOpt(req, r.platform)
|
||||||
resp, err := r.metaResolver.ResolveSourceMetadata(ctx, source, opt)
|
resp, err := r.metaResolver.ResolveSourceMetadata(ctx, source, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return buildSourceMetaResponse(resp), nil
|
return sourcemeta.ToGatewayMetaResponse(resp), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *policyTestOptionsProvider) init(ctx context.Context) error {
|
func (r *policyTestOptionsProvider) init(ctx context.Context) error {
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ require (
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||||
github.com/oklog/ulid v1.3.1 // indirect
|
github.com/oklog/ulid v1.3.1 // indirect
|
||||||
|
github.com/package-url/packageurl-go v0.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/containerd/platforms"
|
||||||
|
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
||||||
|
"github.com/moby/buildkit/solver/pb"
|
||||||
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxMaterialDepth = 24
|
||||||
|
|
||||||
|
func SourceToInput(ctx context.Context, verifier PolicyVerifierProvider, src *gwpb.ResolveSourceMetaResponse, platform *ocispecs.Platform, logf func(logrus.Level, string)) (Input, error) {
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
return sourceToInputRecursive(ctx, verifier, src, platform, 0, seen, logf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sourceToInputRecursive(ctx context.Context, verifier PolicyVerifierProvider, src *gwpb.ResolveSourceMetaResponse, platform *ocispecs.Platform, depth int, seen map[string]struct{}, logf func(logrus.Level, string)) (Input, error) {
|
||||||
|
if depth > maxMaterialDepth {
|
||||||
|
return Input{}, errors.Errorf("provenance materials depth exceeds limit %d", maxMaterialDepth)
|
||||||
|
}
|
||||||
|
if src == nil || src.Source == nil {
|
||||||
|
return Input{}, errors.New("source metadata response is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
inp, unknowns, err := sourceToInput(ctx, verifier, src, platform, logf)
|
||||||
|
if err != nil {
|
||||||
|
return Input{}, err
|
||||||
|
}
|
||||||
|
inp.setUnknowns(unknowns)
|
||||||
|
inp.Env.Depth = depth
|
||||||
|
|
||||||
|
if inp.Image == nil || inp.Image.Provenance == nil || len(inp.Image.Provenance.materialsRaw) == 0 {
|
||||||
|
return inp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key := sourceUniqueIdentifier(src.Source, platform)
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
return Input{}, nil
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
defer delete(seen, key)
|
||||||
|
|
||||||
|
materials := make([]Input, 0, len(inp.Image.Provenance.materialsRaw))
|
||||||
|
for _, m := range inp.Image.Provenance.materialsRaw {
|
||||||
|
matSrc, matPlatform, err := parseSLSAMaterial(m)
|
||||||
|
if err != nil {
|
||||||
|
materials = append(materials, Input{})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if matSrc == nil {
|
||||||
|
materials = append(materials, Input{})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
matResp := &gwpb.ResolveSourceMetaResponse{Source: matSrc}
|
||||||
|
child, err := sourceToInputRecursive(ctx, verifier, matResp, firstNonNilPlatform(matPlatform, platform), depth+1, seen, logf)
|
||||||
|
if err != nil {
|
||||||
|
return Input{}, errors.Wrapf(err, "failed to build material input for %q", m.URI)
|
||||||
|
}
|
||||||
|
materials = append(materials, child)
|
||||||
|
}
|
||||||
|
inp.Image.Provenance.Materials = materials
|
||||||
|
return inp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sourceUniqueIdentifier is used only for recursive cycle detection safety.
|
||||||
|
func sourceUniqueIdentifier(src *pb.SourceOp, platform *ocispecs.Platform) string {
|
||||||
|
if src == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
key := src.Identifier
|
||||||
|
if platform != nil {
|
||||||
|
key += "|" + platforms.Format(*platform)
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstNonNilPlatform(values ...*ocispecs.Platform) *ocispecs.Platform {
|
||||||
|
for _, v := range values {
|
||||||
|
if v != nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (inp *Input) setUnknowns(unknowns []string) {
|
||||||
|
if inp == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(unknowns) == 0 {
|
||||||
|
inp.unknowns = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(unknowns))
|
||||||
|
for _, u := range unknowns {
|
||||||
|
v := strings.TrimPrefix(u, "input.")
|
||||||
|
if v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
inp.unknowns = out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (inp Input) Unknowns() []string {
|
||||||
|
var refs []string
|
||||||
|
collectInputUnknowns(inp, "input", &refs)
|
||||||
|
return refs
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectInputUnknowns(inp Input, prefix string, refs *[]string) {
|
||||||
|
for _, u := range inp.unknowns {
|
||||||
|
if u == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
*refs = append(*refs, prefix+"."+u)
|
||||||
|
}
|
||||||
|
if inp.Image == nil || inp.Image.Provenance == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range inp.Image.Provenance.Materials {
|
||||||
|
childPrefix := fmt.Sprintf("%s.image.provenance.materials[%d]", prefix, i)
|
||||||
|
collectInputUnknowns(inp.Image.Provenance.Materials[i], childPrefix, refs)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInputUnknownRefsNestedMaterials(t *testing.T) {
|
||||||
|
inp := Input{
|
||||||
|
Image: &Image{
|
||||||
|
Provenance: &ImageProvenance{
|
||||||
|
Materials: []Input{
|
||||||
|
{
|
||||||
|
Image: &Image{
|
||||||
|
Provenance: &ImageProvenance{
|
||||||
|
Materials: []Input{{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
inp.setUnknowns([]string{"input.image.provenance"})
|
||||||
|
inp.Image.Provenance.Materials[0].setUnknowns([]string{"input.image.hasProvenance"})
|
||||||
|
inp.Image.Provenance.Materials[0].Image.Provenance.Materials[0].setUnknowns([]string{"input.git.commit"})
|
||||||
|
|
||||||
|
require.Equal(t, []string{
|
||||||
|
"input.image.provenance",
|
||||||
|
"input.image.provenance.materials[0].image.hasProvenance",
|
||||||
|
"input.image.provenance.materials[0].image.provenance.materials[0].git.commit",
|
||||||
|
}, inp.Unknowns())
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/distribution/reference"
|
||||||
|
"github.com/docker/buildx/util/urlutil"
|
||||||
|
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
|
||||||
|
"github.com/moby/buildkit/solver/pb"
|
||||||
|
"github.com/moby/buildkit/util/gitutil"
|
||||||
|
"github.com/moby/buildkit/util/purl"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isMaterialKey(key string) (idx int, rest string, ok bool) {
|
||||||
|
const prefix = "image.provenance.materials["
|
||||||
|
if !strings.HasPrefix(key, prefix) {
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
rest = strings.TrimPrefix(key, prefix)
|
||||||
|
end := strings.IndexByte(rest, ']')
|
||||||
|
if end < 0 {
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
n, err := strconv.Atoi(rest[:end])
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
rest = strings.TrimPrefix(rest[end+1:], ".")
|
||||||
|
return n, rest, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSLSAMaterial(m slsa1.ResourceDescriptor) (*pb.SourceOp, *ocispecs.Platform, error) {
|
||||||
|
uri := m.URI
|
||||||
|
dgst := m.Digest
|
||||||
|
if strings.HasPrefix(uri, "pkg:docker/") {
|
||||||
|
return dockerMaterialSource(uri, dgst)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gu, err := gitutil.ParseURL(uri); err == nil {
|
||||||
|
if strings.HasSuffix(strings.ToLower(gu.Path), ".git") || gu.Scheme != gitutil.HTTPProtocol && gu.Scheme != gitutil.HTTPSProtocol {
|
||||||
|
return gitMaterialSource(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlutil.IsHTTPURL(uri) {
|
||||||
|
return &pb.SourceOp{Identifier: uri}, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, errors.Errorf("unsupported material URI %q", uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dockerMaterialSource(uri string, dgst map[string]string) (*pb.SourceOp, *ocispecs.Platform, error) {
|
||||||
|
refStr, platform, err := purl.PURLToRef(uri)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
named, err := reference.ParseNormalizedNamed(refStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "invalid docker reference %q from %q", refStr, uri)
|
||||||
|
}
|
||||||
|
if checksum := strings.TrimSpace(dgst["sha256"]); checksum != "" {
|
||||||
|
dgstRef, err := digest.Parse(checksum)
|
||||||
|
if err != nil {
|
||||||
|
dgstRef, err = digest.Parse("sha256:" + checksum)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "invalid material digest %q for %q", checksum, uri)
|
||||||
|
}
|
||||||
|
if canonical, ok := named.(reference.Canonical); ok {
|
||||||
|
if canonical.Digest() != dgstRef {
|
||||||
|
return nil, nil, errors.Errorf("material digest mismatch for %q: ref has %s but provenance has %s", uri, canonical.Digest(), dgstRef)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
named, err = reference.WithDigest(named, dgstRef)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "failed to add digest %q to %q", dgstRef, refStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.SourceOp{Identifier: "docker-image://" + named.String()}, platform, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitMaterialSource(uri string) (*pb.SourceOp, *ocispecs.Platform, error) {
|
||||||
|
gu, err := gitutil.ParseURL(uri)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch gu.Scheme {
|
||||||
|
case gitutil.HTTPSProtocol, gitutil.HTTPProtocol, gitutil.SSHProtocol, gitutil.GitProtocol:
|
||||||
|
default:
|
||||||
|
return nil, nil, errors.Errorf("unsupported git material URI %q", uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
id := gu.Host + path.Join("/", gu.Path)
|
||||||
|
if gu.Opts != nil && (gu.Opts.Ref != "" || gu.Opts.Subdir != "") {
|
||||||
|
id += "#" + gu.Opts.Ref
|
||||||
|
if gu.Opts.Subdir != "" {
|
||||||
|
id += ":" + gu.Opts.Subdir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.SourceOp{
|
||||||
|
Identifier: "git://" + id,
|
||||||
|
Attrs: map[string]string{
|
||||||
|
pb.AttrFullRemoteURL: uri,
|
||||||
|
},
|
||||||
|
}, nil, nil
|
||||||
|
}
|
||||||
+64
-13
@@ -2,6 +2,8 @@ package policy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -10,6 +12,7 @@ import (
|
|||||||
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
|
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
|
||||||
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
||||||
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
|
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const predicateTypeAnnotation = "in-toto.io/predicate-type"
|
const predicateTypeAnnotation = "in-toto.io/predicate-type"
|
||||||
@@ -24,12 +27,11 @@ type inTotoStatement struct {
|
|||||||
Predicate json.RawMessage `json:"predicate"`
|
Predicate json.RawMessage `json:"predicate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseProvenance(ac *gwpb.AttestationChain) (*ImageProvenance, error) {
|
func parseProvenance(ac *gwpb.AttestationChain, logf func(logrus.Level, string)) (*ImageProvenance, error) {
|
||||||
if ac == nil || len(ac.Blobs) == 0 {
|
if ac == nil || len(ac.Blobs) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer blobs that explicitly declare a provenance predicate type.
|
|
||||||
for _, b := range ac.Blobs {
|
for _, b := range ac.Blobs {
|
||||||
if b == nil || b.Descriptor_ == nil || len(b.Data) == 0 {
|
if b == nil || b.Descriptor_ == nil || len(b.Data) == 0 {
|
||||||
continue
|
continue
|
||||||
@@ -41,7 +43,7 @@ func parseProvenance(ac *gwpb.AttestationChain) (*ImageProvenance, error) {
|
|||||||
if !slices.Contains(resolveProvenanceAttestations, pt) {
|
if !slices.Contains(resolveProvenanceAttestations, pt) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
prv, err := parseProvenanceBlob(b.Data, pt)
|
prv, err := parseProvenanceBlob(b.Data, pt, logf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -53,25 +55,30 @@ func parseProvenance(ac *gwpb.AttestationChain) (*ImageProvenance, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseProvenanceBlob(dt []byte, pt string) (*ImageProvenance, error) {
|
func parseProvenanceBlob(dt []byte, pt string, logf func(logrus.Level, string)) (*ImageProvenance, error) {
|
||||||
var stmt inTotoStatement
|
var stmt inTotoStatement
|
||||||
if err := json.Unmarshal(dt, &stmt); err != nil || len(stmt.Predicate) == 0 {
|
if err := json.Unmarshal(dt, &stmt); err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if stmt.PredicateType != "" && stmt.PredicateType != pt {
|
if len(stmt.Predicate) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch pt {
|
predicateType := stmt.PredicateType
|
||||||
case slsa1.PredicateSLSAProvenance:
|
if predicateType == "" {
|
||||||
return parseSLSA1Provenance(stmt.Predicate)
|
predicateType = pt
|
||||||
case slsa02.PredicateSLSAProvenance:
|
|
||||||
return parseSLSA02Provenance(stmt.Predicate)
|
|
||||||
}
|
}
|
||||||
|
switch predicateType {
|
||||||
|
case slsa1.PredicateSLSAProvenance:
|
||||||
|
return parseSLSA1Provenance(stmt.Predicate, logf)
|
||||||
|
case slsa02.PredicateSLSAProvenance:
|
||||||
|
return parseSLSA02Provenance(stmt.Predicate, logf)
|
||||||
|
default:
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSLSA1Provenance(dt []byte) (*ImageProvenance, error) {
|
func parseSLSA1Provenance(dt []byte, logf func(logrus.Level, string)) (*ImageProvenance, error) {
|
||||||
var pred provenancetypes.ProvenancePredicateSLSA1
|
var pred provenancetypes.ProvenancePredicateSLSA1
|
||||||
if err := json.Unmarshal(dt, &pred); err != nil {
|
if err := json.Unmarshal(dt, &pred); err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -92,6 +99,7 @@ func parseSLSA1Provenance(dt []byte) (*ImageProvenance, error) {
|
|||||||
BuildArgs: extractBuildArgs(pred.BuildDefinition.ExternalParameters.Request.Args),
|
BuildArgs: extractBuildArgs(pred.BuildDefinition.ExternalParameters.Request.Args),
|
||||||
RawArgs: pred.BuildDefinition.ExternalParameters.Request.Args,
|
RawArgs: pred.BuildDefinition.ExternalParameters.Request.Args,
|
||||||
}
|
}
|
||||||
|
prv.materialsRaw = rawMaterialsFromSLSA1(pred.BuildDefinition.ResolvedDependencies, logf)
|
||||||
|
|
||||||
if md := pred.RunDetails.Metadata; md != nil {
|
if md := pred.RunDetails.Metadata; md != nil {
|
||||||
prv.InvocationID = md.InvocationID
|
prv.InvocationID = md.InvocationID
|
||||||
@@ -108,7 +116,7 @@ func parseSLSA1Provenance(dt []byte) (*ImageProvenance, error) {
|
|||||||
return prv, nil
|
return prv, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSLSA02Provenance(dt []byte) (*ImageProvenance, error) {
|
func parseSLSA02Provenance(dt []byte, logf func(logrus.Level, string)) (*ImageProvenance, error) {
|
||||||
var pred provenancetypes.ProvenancePredicateSLSA02
|
var pred provenancetypes.ProvenancePredicateSLSA02
|
||||||
if err := json.Unmarshal(dt, &pred); err != nil {
|
if err := json.Unmarshal(dt, &pred); err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -129,6 +137,7 @@ func parseSLSA02Provenance(dt []byte) (*ImageProvenance, error) {
|
|||||||
BuildArgs: extractBuildArgs(pred.Invocation.Parameters.Args),
|
BuildArgs: extractBuildArgs(pred.Invocation.Parameters.Args),
|
||||||
RawArgs: pred.Invocation.Parameters.Args,
|
RawArgs: pred.Invocation.Parameters.Args,
|
||||||
}
|
}
|
||||||
|
prv.materialsRaw = rawMaterialsFromSLSA02(pred.Materials, logf)
|
||||||
|
|
||||||
if md := pred.Metadata; md != nil {
|
if md := pred.Metadata; md != nil {
|
||||||
prv.InvocationID = md.BuildInvocationID
|
prv.InvocationID = md.BuildInvocationID
|
||||||
@@ -146,6 +155,48 @@ func parseSLSA02Provenance(dt []byte) (*ImageProvenance, error) {
|
|||||||
return prv, nil
|
return prv, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rawMaterialsFromSLSA1(materials []slsa1.ResourceDescriptor, logf func(logrus.Level, string)) []slsa1.ResourceDescriptor {
|
||||||
|
if len(materials) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]slsa1.ResourceDescriptor, 0, len(materials))
|
||||||
|
for _, m := range materials {
|
||||||
|
rd := slsa1.ResourceDescriptor{
|
||||||
|
URI: m.URI,
|
||||||
|
Digest: maps.Clone(m.Digest),
|
||||||
|
}
|
||||||
|
if _, _, err := parseSLSAMaterial(rd); err != nil {
|
||||||
|
if logf != nil {
|
||||||
|
logf(logrus.WarnLevel, fmt.Sprintf("skipping unsupported provenance material %q: %v", m.URI, err))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, rd)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func rawMaterialsFromSLSA02(materials []slsa02.ProvenanceMaterial, logf func(logrus.Level, string)) []slsa1.ResourceDescriptor {
|
||||||
|
if len(materials) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]slsa1.ResourceDescriptor, 0, len(materials))
|
||||||
|
for _, m := range materials {
|
||||||
|
rd := slsa1.ResourceDescriptor{
|
||||||
|
URI: m.URI,
|
||||||
|
Digest: maps.Clone(m.Digest),
|
||||||
|
}
|
||||||
|
if _, _, err := parseSLSAMaterial(rd); err != nil {
|
||||||
|
if logf != nil {
|
||||||
|
logf(logrus.WarnLevel, fmt.Sprintf("skipping unsupported provenance material %q: %v", m.URI, err))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, rd)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func boolPtr(v bool) *bool {
|
func boolPtr(v bool) *bool {
|
||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/buildx/util/sourcemeta"
|
||||||
|
"github.com/moby/buildkit/client/llb/sourceresolver"
|
||||||
|
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
||||||
|
"github.com/moby/buildkit/solver/pb"
|
||||||
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SourceMetadataResolver interface {
|
||||||
|
ResolveSourceMetadata(context.Context, *pb.SourceOp, sourceresolver.Opt) (*sourceresolver.MetaResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveInputUnknowns(ctx context.Context, input *Input, rootSource *pb.SourceOp, unknowns []string, rootPlatform *pb.Platform, defaultPlatform *ocispecs.Platform, resolver SourceMetadataResolver, verifier PolicyVerifierProvider, logf func(logrus.Level, string)) (bool, *gwpb.ResolveSourceMetaRequest, error) {
|
||||||
|
if input == nil || len(unknowns) == 0 {
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
return resolveNodeUnknowns(ctx, input, rootSource, defaultPlatform, normalizeNodeUnknowns(unknowns), rootPlatform, defaultPlatform, resolver, verifier, logf, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveNodeUnknowns(ctx context.Context, node *Input, source *pb.SourceOp, nodePlatform *ocispecs.Platform, unknowns []string, rootPlatform *pb.Platform, defaultPlatform *ocispecs.Platform, resolver SourceMetadataResolver, verifier PolicyVerifierProvider, logf func(logrus.Level, string), setNode func(Input) error) (bool, *gwpb.ResolveSourceMetaRequest, error) {
|
||||||
|
directUnknowns, childUnknowns := splitNodeUnknowns(unknowns)
|
||||||
|
if len(directUnknowns) > 0 {
|
||||||
|
req, err := sourceResolveRequest(source, nodePlatform, setNode == nil, directUnknowns, rootPlatform, logf)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
if req != nil {
|
||||||
|
if setNode == nil {
|
||||||
|
return false, req, nil
|
||||||
|
}
|
||||||
|
if resolver == nil {
|
||||||
|
return false, nil, errors.Errorf("material metadata resolution requires source resolver")
|
||||||
|
}
|
||||||
|
resp, err := resolveSourceMetaWithResolver(ctx, resolver, req, defaultPlatform)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, errors.Wrap(err, "failed to resolve source metadata for material")
|
||||||
|
}
|
||||||
|
nextInput, err := SourceToInput(ctx, verifier, resp, nodePlatform, logf)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, errors.Wrap(err, "failed to rebuild material input")
|
||||||
|
}
|
||||||
|
if err := setNode(nextInput); err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
return true, nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, childNodeUnknowns := range childUnknowns {
|
||||||
|
if node == nil || node.Image == nil || node.Image.Provenance == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if idx < 0 || idx >= len(node.Image.Provenance.Materials) || idx >= len(node.Image.Provenance.materialsRaw) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
raw := node.Image.Provenance.materialsRaw[idx]
|
||||||
|
childSource, childNodePlatform, err := parseSLSAMaterial(raw)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
childPlatform := firstNonNilPlatform(childNodePlatform, nodePlatform)
|
||||||
|
retry, next, err := resolveNodeUnknowns(ctx, &node.Image.Provenance.Materials[idx], childSource, childPlatform, childNodeUnknowns, rootPlatform, defaultPlatform, resolver, verifier, logf, func(next Input) error {
|
||||||
|
node.Image.Provenance.Materials[idx] = next
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
if retry || next != nil {
|
||||||
|
return retry, next, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitNodeUnknowns(unknowns []string) ([]string, map[int][]string) {
|
||||||
|
child := map[int][]string{}
|
||||||
|
var direct []string
|
||||||
|
for _, u := range unknowns {
|
||||||
|
if u == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if idx, rest, ok := isMaterialKey(u); ok {
|
||||||
|
if rest == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !slices.Contains(child[idx], rest) {
|
||||||
|
child[idx] = append(child[idx], rest)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !slices.Contains(direct, u) {
|
||||||
|
direct = append(direct, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return direct, child
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeNodeUnknowns(unknowns []string) []string {
|
||||||
|
out := make([]string, 0, len(unknowns))
|
||||||
|
for _, u := range unknowns {
|
||||||
|
v := strings.TrimPrefix(u, "input.")
|
||||||
|
if v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !slices.Contains(out, v) {
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func sourceResolveRequest(source *pb.SourceOp, nodePlatform *ocispecs.Platform, rootNode bool, fields []string, rootPlatform *pb.Platform, logf func(logrus.Level, string)) (*gwpb.ResolveSourceMetaRequest, error) {
|
||||||
|
if source == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
req := &gwpb.ResolveSourceMetaRequest{Source: source}
|
||||||
|
if nodePlatform != nil {
|
||||||
|
req.Platform = &pb.Platform{OS: nodePlatform.OS, Architecture: nodePlatform.Architecture, Variant: nodePlatform.Variant}
|
||||||
|
} else if rootNode {
|
||||||
|
req.Platform = rootPlatform
|
||||||
|
}
|
||||||
|
if err := AddUnknownsWithLogger(logf, req, fields); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req.Image == nil && req.Git == nil && !hasHTTPUnknowns(fields) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveSourceMetaWithResolver(ctx context.Context, resolver SourceMetadataResolver, req *gwpb.ResolveSourceMetaRequest, defaultPlatform *ocispecs.Platform) (*gwpb.ResolveSourceMetaResponse, error) {
|
||||||
|
if resolver == nil {
|
||||||
|
return nil, errors.New("source resolver is not configured")
|
||||||
|
}
|
||||||
|
if req == nil || req.Source == nil {
|
||||||
|
return nil, errors.New("source metadata request is missing source")
|
||||||
|
}
|
||||||
|
resp, err := resolver.ResolveSourceMetadata(ctx, req.Source, sourcemeta.ToResolverOpt(req, defaultPlatform))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sourcemeta.ToGatewayMetaResponse(resp), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
|
||||||
|
"github.com/moby/buildkit/client/llb/sourceresolver"
|
||||||
|
"github.com/moby/buildkit/solver/pb"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stubSourceResolver struct {
|
||||||
|
resolveFn func(context.Context, *pb.SourceOp, sourceresolver.Opt) (*sourceresolver.MetaResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stubSourceResolver) ResolveSourceMetadata(ctx context.Context, op *pb.SourceOp, opt sourceresolver.Opt) (*sourceresolver.MetaResponse, error) {
|
||||||
|
return s.resolveFn(ctx, op, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveInputUnknownsResolvesMaterialField(t *testing.T) {
|
||||||
|
inp := Input{
|
||||||
|
Image: &Image{
|
||||||
|
Provenance: &ImageProvenance{
|
||||||
|
Materials: []Input{
|
||||||
|
{Image: &Image{Ref: "docker.io/library/alpine:3.20"}},
|
||||||
|
},
|
||||||
|
materialsRaw: []slsa1.ResourceDescriptor{
|
||||||
|
{URI: "pkg:docker/library/alpine@3.20?platform=linux/amd64"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
inp.Image.Provenance.Materials[0].setUnknowns([]string{"input.image.hasProvenance"})
|
||||||
|
|
||||||
|
resolver := stubSourceResolver{
|
||||||
|
resolveFn: func(_ context.Context, op *pb.SourceOp, _ sourceresolver.Opt) (*sourceresolver.MetaResponse, error) {
|
||||||
|
require.Equal(t, "docker-image://docker.io/library/alpine:3.20", op.Identifier)
|
||||||
|
return &sourceresolver.MetaResponse{
|
||||||
|
Op: op,
|
||||||
|
Image: &sourceresolver.ResolveImageResponse{
|
||||||
|
Digest: digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
|
||||||
|
AttestationChain: &sourceresolver.AttestationChain{
|
||||||
|
AttestationManifest: digest.Digest("sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
retry, next, err := ResolveInputUnknowns(
|
||||||
|
context.Background(),
|
||||||
|
&inp,
|
||||||
|
&pb.SourceOp{Identifier: "docker-image://docker.io/library/busybox:latest"},
|
||||||
|
[]string{"image.provenance.materials[0].image.hasProvenance"},
|
||||||
|
&pb.Platform{OS: "linux", Architecture: "amd64"},
|
||||||
|
&ocispecs.Platform{OS: "linux", Architecture: "amd64"},
|
||||||
|
resolver,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, retry)
|
||||||
|
require.Nil(t, next)
|
||||||
|
require.True(t, inp.Image.Provenance.Materials[0].Image.HasProvenance)
|
||||||
|
}
|
||||||
+1
-1
@@ -454,7 +454,7 @@ func resolveTestInput(ctx context.Context, files []File, resolver *TestOptionsPr
|
|||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
if next == nil {
|
if next == nil {
|
||||||
inp, _, err := SourceToInputWithLogger(ctx, resolver.VerifierProvider, srcReq, platform, nil)
|
inp, _, err := sourceToInput(ctx, resolver.VerifierProvider, srcReq, platform, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package policy
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
|
||||||
"github.com/moby/buildkit/util/gitutil/gitobject"
|
"github.com/moby/buildkit/util/gitutil/gitobject"
|
||||||
policytypes "github.com/moby/policy-helpers/types"
|
policytypes "github.com/moby/policy-helpers/types"
|
||||||
)
|
)
|
||||||
@@ -13,6 +14,8 @@ type Input struct {
|
|||||||
Image *Image `json:"image,omitempty"`
|
Image *Image `json:"image,omitempty"`
|
||||||
HTTP *HTTP `json:"http,omitempty"`
|
HTTP *HTTP `json:"http,omitempty"`
|
||||||
Git *Git `json:"git,omitempty"`
|
Git *Git `json:"git,omitempty"`
|
||||||
|
|
||||||
|
unknowns []string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Decision struct {
|
type Decision struct {
|
||||||
@@ -25,6 +28,7 @@ type Env struct {
|
|||||||
Labels map[string]string `json:"labels,omitempty"`
|
Labels map[string]string `json:"labels,omitempty"`
|
||||||
Filename string `json:"filename,omitempty"`
|
Filename string `json:"filename,omitempty"`
|
||||||
Target string `json:"target,omitempty"`
|
Target string `json:"target,omitempty"`
|
||||||
|
Depth int `json:"depth"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTP struct {
|
type HTTP struct {
|
||||||
@@ -149,6 +153,9 @@ type ImageProvenance struct {
|
|||||||
Hermetic *bool `json:"hermetic,omitempty"`
|
Hermetic *bool `json:"hermetic,omitempty"`
|
||||||
|
|
||||||
Completeness *ImageProvenanceCompleteness `json:"completeness,omitempty"`
|
Completeness *ImageProvenanceCompleteness `json:"completeness,omitempty"`
|
||||||
|
Materials []Input `json:"materials,omitempty"`
|
||||||
|
|
||||||
|
materialsRaw []slsa1.ResourceDescriptor `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageProvenanceConfigSource struct {
|
type ImageProvenanceConfigSource struct {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ func TestTrimKey(t *testing.T) {
|
|||||||
{"git.tag[0]", "git.tag"},
|
{"git.tag[0]", "git.tag"},
|
||||||
{"input.git.tag.author", "git.tag"},
|
{"input.git.tag.author", "git.tag"},
|
||||||
{"input.git.tag[0]", "git.tag"},
|
{"input.git.tag[0]", "git.tag"},
|
||||||
|
{"input.image.provenance.materials[0].image.hasProvenance", "image.provenance.materials[0].image.hasProvenance"},
|
||||||
|
{"image.provenance.materials[0].image.labels", "image.provenance.materials[0].image.labels"},
|
||||||
|
|
||||||
{"a.b.c", "a.b"},
|
{"a.b.c", "a.b"},
|
||||||
}
|
}
|
||||||
@@ -46,16 +48,17 @@ func TestCollectUnknowns(t *testing.T) {
|
|||||||
p if {
|
p if {
|
||||||
input.git.tag[0].author == "a"
|
input.git.tag[0].author == "a"
|
||||||
input.image.signatures[_].signer.certificateIssuer != ""
|
input.image.signatures[_].signer.certificateIssuer != ""
|
||||||
|
input.image.provenance.materials[0].image.hasProvenance
|
||||||
data.foo.bar == 1
|
data.foo.bar == 1
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
all := collectUnknowns([]*ast.Module{mod}, nil)
|
all := collectUnknowns([]*ast.Module{mod}, nil)
|
||||||
require.ElementsMatch(t, []string{"git.tag", "image.signatures"}, all)
|
require.ElementsMatch(t, []string{"git.tag", "image.signatures", "image.provenance.materials[0].image.hasProvenance"}, all)
|
||||||
|
|
||||||
filtered := collectUnknowns([]*ast.Module{mod}, []string{"input.image.signatures"})
|
filtered := collectUnknowns([]*ast.Module{mod}, []string{"input.image.signatures", "input.image.provenance.materials[0].image.hasProvenance"})
|
||||||
require.Equal(t, []string{"image.signatures"}, filtered)
|
require.ElementsMatch(t, []string{"image.signatures", "image.provenance.materials[0].image.hasProvenance"}, filtered)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRuntimeUnknownInputRefs(t *testing.T) {
|
func TestRuntimeUnknownInputRefs(t *testing.T) {
|
||||||
|
|||||||
+79
-49
@@ -39,6 +39,8 @@ type Policy struct {
|
|||||||
denyIdentifiers map[string]struct{}
|
denyIdentifiers map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxResolveIterations = 10
|
||||||
|
|
||||||
type state struct {
|
type state struct {
|
||||||
Input Input
|
Input Input
|
||||||
Unknowns map[string]struct{}
|
Unknowns map[string]struct{}
|
||||||
@@ -131,7 +133,7 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
|||||||
if req.Source == nil || req.Source.Source == nil {
|
if req.Source == nil || req.Source.Source == nil {
|
||||||
return nil, nil, errors.Errorf("no source info in request")
|
return nil, nil, errors.Errorf("no source info in request")
|
||||||
}
|
}
|
||||||
src := req.Source
|
|
||||||
var platform *ocispecs.Platform
|
var platform *ocispecs.Platform
|
||||||
if req.Platform != nil {
|
if req.Platform != nil {
|
||||||
pl, err := platformFromReq(req)
|
pl, err := platformFromReq(req)
|
||||||
@@ -143,17 +145,15 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
|||||||
platform = p.opt.DefaultPlatform
|
platform = p.opt.DefaultPlatform
|
||||||
}
|
}
|
||||||
|
|
||||||
inp, unknowns, err := SourceToInputWithLogger(ctx, p.opt.VerifierProvider, src, platform, p.opt.Log)
|
inp, err := SourceToInput(ctx, p.opt.VerifierProvider, req.Source, platform, p.opt.Log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.Wrapf(err, "failed to convert source to policy input")
|
return nil, nil, errors.Wrap(err, "failed to build policy input")
|
||||||
}
|
}
|
||||||
inp.Env = p.opt.Env
|
|
||||||
|
|
||||||
caps := &ast.Capabilities{
|
caps := &ast.Capabilities{
|
||||||
Builtins: builtins(),
|
Builtins: builtins(),
|
||||||
Features: slices.Clone(ast.Features),
|
Features: slices.Clone(ast.Features),
|
||||||
}
|
}
|
||||||
|
|
||||||
comp := ast.NewCompiler().WithCapabilities(caps).WithKeepModules(true)
|
comp := ast.NewCompiler().WithCapabilities(caps).WithKeepModules(true)
|
||||||
if p.opt.Log != nil {
|
if p.opt.Log != nil {
|
||||||
comp = comp.WithEnablePrintStatements(true)
|
comp = comp.WithEnablePrintStatements(true)
|
||||||
@@ -170,7 +170,6 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
|||||||
|
|
||||||
var root fs.StatFS
|
var root fs.StatFS
|
||||||
var closeFS func() error
|
var closeFS func() error
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if closeFS != nil {
|
if closeFS != nil {
|
||||||
closeFS()
|
closeFS()
|
||||||
@@ -210,7 +209,6 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "failed to parse imported policy file %s for module %s", fn, k)
|
return nil, errors.Wrapf(err, "failed to parse imported policy file %s for module %s", fn, k)
|
||||||
}
|
}
|
||||||
// rewrite package to be less strict
|
|
||||||
pkgParts := strings.Split(pkgPath, ".")
|
pkgParts := strings.Split(pkgPath, ".")
|
||||||
ref := ast.Ref{mod.Package.Path[0]}
|
ref := ast.Ref{mod.Package.Path[0]}
|
||||||
for _, p := range pkgParts {
|
for _, p := range pkgParts {
|
||||||
@@ -224,42 +222,49 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
|||||||
return out, nil
|
return out, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
opts := []func(*rego.Rego){
|
baseOpts := []func(*rego.Rego){
|
||||||
rego.SetRegoVersion(ast.RegoV1),
|
rego.SetRegoVersion(ast.RegoV1),
|
||||||
rego.Query("data.docker.decision"),
|
rego.Query("data.docker.decision"),
|
||||||
rego.Input(inp),
|
|
||||||
rego.SkipPartialNamespace(true),
|
rego.SkipPartialNamespace(true),
|
||||||
rego.Compiler(comp),
|
rego.Compiler(comp),
|
||||||
|
rego.Module(builtinPolicyModuleFilename, builtinPolicyModule),
|
||||||
}
|
}
|
||||||
if p.opt.Log != nil {
|
if p.opt.Log != nil {
|
||||||
opts = append(opts,
|
baseOpts = append(baseOpts,
|
||||||
rego.EnablePrintStatements(true),
|
rego.EnablePrintStatements(true),
|
||||||
rego.PrintHook(p),
|
rego.PrintHook(p),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
st := &state{
|
for _, file := range p.opt.Files {
|
||||||
Input: inp,
|
baseOpts = append(baseOpts, rego.Module(file.Filename, string(file.Data)))
|
||||||
}
|
|
||||||
for _, f := range p.funcs {
|
|
||||||
opts = append(opts, f.impl(st))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts = append(opts, rego.Module(builtinPolicyModuleFilename, builtinPolicyModule))
|
p.log(logrus.InfoLevel, "checking policy for source %s", sourceName(req))
|
||||||
for _, file := range p.opt.Files {
|
|
||||||
opts = append(opts, rego.Module(file.Filename, string(file.Data)))
|
for range maxResolveIterations {
|
||||||
|
runInput := inp
|
||||||
|
applyEnvWithDepth(&runInput, p.opt.Env, 0)
|
||||||
|
|
||||||
|
runOpts := append([]func(*rego.Rego){}, baseOpts...)
|
||||||
|
runOpts = append(runOpts, rego.Input(runInput))
|
||||||
|
|
||||||
|
st := &state{Input: runInput}
|
||||||
|
for _, f := range p.funcs {
|
||||||
|
runOpts = append(runOpts, f.impl(st))
|
||||||
}
|
}
|
||||||
dt, err := json.MarshalIndent(inp, "", " ")
|
|
||||||
|
dt, err := json.MarshalIndent(runInput, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.Wrapf(err, "failed to marshal policy input")
|
return nil, nil, errors.Wrapf(err, "failed to marshal policy input")
|
||||||
}
|
}
|
||||||
p.log(logrus.InfoLevel, "checking policy for source %s", sourceName(req))
|
|
||||||
p.log(logrus.DebugLevel, "policy input: %s", dt)
|
p.log(logrus.DebugLevel, "policy input: %s", dt)
|
||||||
|
|
||||||
|
unknowns := inp.Unknowns()
|
||||||
if len(unknowns) > 0 {
|
if len(unknowns) > 0 {
|
||||||
p.log(logrus.DebugLevel, "unknowns for policy evaluation: %+v", unknowns)
|
p.log(logrus.DebugLevel, "unknowns for policy evaluation: %+v", summarizeUnknownsForLog(unknowns))
|
||||||
opts = append(opts, rego.Unknowns(unknowns))
|
runOpts = append(runOpts, rego.Unknowns(unknowns))
|
||||||
}
|
}
|
||||||
r := rego.New(opts...)
|
r := rego.New(runOpts...)
|
||||||
|
|
||||||
if len(unknowns) > 0 {
|
if len(unknowns) > 0 {
|
||||||
pq, err := r.Partial(ctx)
|
pq, err := r.Partial(ctx)
|
||||||
@@ -269,41 +274,33 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
|||||||
unk := collectUnknowns(pq.Support, unknowns)
|
unk := collectUnknowns(pq.Support, unknowns)
|
||||||
unk = append(unk, runtimeUnknownInputRefs(st)...)
|
unk = append(unk, runtimeUnknownInputRefs(st)...)
|
||||||
|
|
||||||
if len(unk) > 0 {
|
retry, next, err := p.resolveUnknowns(ctx, &inp, req, platform, unk)
|
||||||
next := &gwpb.ResolveSourceMetaRequest{
|
if err != nil {
|
||||||
Source: req.Source.Source,
|
|
||||||
Platform: req.Platform,
|
|
||||||
}
|
|
||||||
if err := AddUnknownsWithLogger(p.opt.Log, next, unk); err != nil {
|
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if next.Image != nil || next.Git != nil || hasHTTPUnknowns(unk) {
|
if next != nil {
|
||||||
p.log(logrus.InfoLevel, "policy decision for source %s: resolve missing fields %+v", sourceName(req), summarizeUnknownsForLog(unk))
|
|
||||||
return nil, next, nil
|
return nil, next, nil
|
||||||
}
|
}
|
||||||
|
if retry {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
st.ImagePins = nil
|
st.ImagePins = nil
|
||||||
|
|
||||||
rs, err := r.Eval(ctx)
|
rs, err := r.Eval(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rtUnk := runtimeUnknownInputRefs(st)
|
retry, next, err := p.resolveUnknowns(ctx, &inp, req, platform, runtimeUnknownInputRefs(st))
|
||||||
if len(rtUnk) > 0 {
|
if err != nil {
|
||||||
next := &gwpb.ResolveSourceMetaRequest{
|
|
||||||
Source: req.Source.Source,
|
|
||||||
Platform: req.Platform,
|
|
||||||
}
|
|
||||||
if err := AddUnknownsWithLogger(p.opt.Log, next, rtUnk); err != nil {
|
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if next.Image != nil || next.Git != nil || hasHTTPUnknowns(rtUnk) {
|
if next != nil {
|
||||||
p.log(logrus.InfoLevel, "policy decision for source %s: resolve missing fields %+v", sourceName(req), summarizeUnknownsForLog(rtUnk))
|
|
||||||
return nil, next, nil
|
return nil, next, nil
|
||||||
}
|
}
|
||||||
|
if retry {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rs) == 0 {
|
if len(rs) == 0 {
|
||||||
@@ -349,7 +346,7 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
|||||||
return nil, nil, errors.Errorf("multiple image pins set to %s: %v", sourceName(req), st.ImagePins)
|
return nil, nil, errors.Errorf("multiple image pins set to %s: %v", sourceName(req), st.ImagePins)
|
||||||
}
|
}
|
||||||
if len(st.ImagePins) == 1 {
|
if len(st.ImagePins) == 1 {
|
||||||
newSrc, err := addPinToImage(src.Source, slices.Collect(maps.Keys(st.ImagePins))[0])
|
newSrc, err := addPinToImage(req.Source.Source, slices.Collect(maps.Keys(st.ImagePins))[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.Wrapf(err, "failed to add image pin to source")
|
return nil, nil, errors.Wrapf(err, "failed to add image pin to source")
|
||||||
}
|
}
|
||||||
@@ -369,10 +366,27 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
|||||||
if resp.Action == moby_buildkit_v1_sourcepolicy.PolicyAction_DENY {
|
if resp.Action == moby_buildkit_v1_sourcepolicy.PolicyAction_DENY {
|
||||||
p.recordDenyIdentifier(req)
|
p.recordDenyIdentifier(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp, nil, nil
|
return resp, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, errors.Errorf("maximum attempts reached for resolving policy metadata")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Policy) resolveUnknowns(ctx context.Context, input *Input, req *policysession.CheckPolicyRequest, defaultPlatform *ocispecs.Platform, unk []string) (bool, *gwpb.ResolveSourceMetaRequest, error) {
|
||||||
|
var resolver SourceMetadataResolver
|
||||||
|
if p.opt.SourceResolver != nil {
|
||||||
|
resolver = p.opt.SourceResolver
|
||||||
|
}
|
||||||
|
retry, next, err := ResolveInputUnknowns(ctx, input, req.Source.Source, unk, req.Platform, defaultPlatform, resolver, p.opt.VerifierProvider, p.opt.Log)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
if next != nil {
|
||||||
|
p.log(logrus.InfoLevel, "policy decision for source %s: resolve missing fields %+v", sourceName(req), summarizeUnknownsForLog(unk))
|
||||||
|
return false, next, nil
|
||||||
|
}
|
||||||
|
return retry, nil, nil
|
||||||
|
}
|
||||||
func platformFromReq(req *policysession.CheckPolicyRequest) (*ocispecs.Platform, error) {
|
func platformFromReq(req *policysession.CheckPolicyRequest) (*ocispecs.Platform, error) {
|
||||||
if req.Platform != nil {
|
if req.Platform != nil {
|
||||||
platformStr := req.Platform.OS + "/" + req.Platform.Architecture
|
platformStr := req.Platform.OS + "/" + req.Platform.Architecture
|
||||||
@@ -404,11 +418,7 @@ func (p *Policy) Print(ctx print.Context, msg string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SourceToInput(ctx context.Context, getVerifier PolicyVerifierProvider, src *gwpb.ResolveSourceMetaResponse, platform *ocispecs.Platform) (Input, []string, error) {
|
func sourceToInput(ctx context.Context, getVerifier PolicyVerifierProvider, src *gwpb.ResolveSourceMetaResponse, platform *ocispecs.Platform, logf func(logrus.Level, string)) (Input, []string, error) {
|
||||||
return SourceToInputWithLogger(ctx, getVerifier, src, platform, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SourceToInputWithLogger(ctx context.Context, getVerifier PolicyVerifierProvider, src *gwpb.ResolveSourceMetaResponse, platform *ocispecs.Platform, logf func(logrus.Level, string)) (Input, []string, error) {
|
|
||||||
var inp Input
|
var inp Input
|
||||||
var unknowns []string
|
var unknowns []string
|
||||||
|
|
||||||
@@ -625,7 +635,9 @@ func SourceToInputWithLogger(ctx context.Context, getVerifier PolicyVerifierProv
|
|||||||
if err := json.Unmarshal(cfg, &img); err != nil {
|
if err := json.Unmarshal(cfg, &img); err != nil {
|
||||||
return inp, nil, errors.Wrapf(err, "failed to unmarshal image config")
|
return inp, nil, errors.Wrapf(err, "failed to unmarshal image config")
|
||||||
}
|
}
|
||||||
|
if img.Created != nil {
|
||||||
inp.Image.CreatedTime = img.Created.Format(time.RFC3339)
|
inp.Image.CreatedTime = img.Created.Format(time.RFC3339)
|
||||||
|
}
|
||||||
inp.Image.Labels = img.Config.Labels
|
inp.Image.Labels = img.Config.Labels
|
||||||
inp.Image.Env = img.Config.Env
|
inp.Image.Env = img.Config.Env
|
||||||
inp.Image.User = img.Config.User
|
inp.Image.User = img.Config.User
|
||||||
@@ -639,7 +651,7 @@ func SourceToInputWithLogger(ctx context.Context, getVerifier PolicyVerifierProv
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ac := src.Image.AttestationChain; ac != nil {
|
if ac := src.Image.AttestationChain; ac != nil {
|
||||||
if prv, err := parseProvenance(ac); err != nil {
|
if prv, err := parseProvenance(ac, logf); err != nil {
|
||||||
if logf != nil {
|
if logf != nil {
|
||||||
logf(logrus.DebugLevel, fmt.Sprintf("failed to parse image provenance: %v", err))
|
logf(logrus.DebugLevel, fmt.Sprintf("failed to parse image provenance: %v", err))
|
||||||
}
|
}
|
||||||
@@ -681,6 +693,20 @@ func withPrefix(arr []string, prefix string) []string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyEnvWithDepth(inp *Input, env Env, depth int) {
|
||||||
|
if inp == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inp.Env = env
|
||||||
|
inp.Env.Depth = depth
|
||||||
|
if inp.Image == nil || inp.Image.Provenance == nil || len(inp.Image.Provenance.Materials) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range inp.Image.Provenance.Materials {
|
||||||
|
applyEnvWithDepth(&inp.Image.Provenance.Materials[i], env, depth+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func AddUnknowns(req *gwpb.ResolveSourceMetaRequest, unk []string) error {
|
func AddUnknowns(req *gwpb.ResolveSourceMetaRequest, unk []string) error {
|
||||||
return AddUnknownsWithLogger(nil, req, unk)
|
return AddUnknownsWithLogger(nil, req, unk)
|
||||||
}
|
}
|
||||||
@@ -811,6 +837,7 @@ func summarizeUnknownsForLog(unk []string) []string {
|
|||||||
out := make([]string, 0, len(unk))
|
out := make([]string, 0, len(unk))
|
||||||
seen := map[string]struct{}{}
|
seen := map[string]struct{}{}
|
||||||
for _, u := range unk {
|
for _, u := range unk {
|
||||||
|
u = strings.TrimPrefix(u, "input.")
|
||||||
if strings.HasPrefix(u, "image.signatures") {
|
if strings.HasPrefix(u, "image.signatures") {
|
||||||
u = "image.signatures"
|
u = "image.signatures"
|
||||||
}
|
}
|
||||||
@@ -849,6 +876,9 @@ func hasHTTPUnknowns(unk []string) bool {
|
|||||||
|
|
||||||
func trimKey(s string) string {
|
func trimKey(s string) string {
|
||||||
s = strings.TrimPrefix(s, "input.")
|
s = strings.TrimPrefix(s, "input.")
|
||||||
|
if strings.HasPrefix(s, "image.provenance.materials[") {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
dot = '.'
|
dot = '.'
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSourceToInputWithLogger(t *testing.T) {
|
func TestSourceToInputSingleSource(t *testing.T) {
|
||||||
tm := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)
|
tm := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -866,7 +866,7 @@ func TestSourceToInputWithLogger(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
inp, unknowns, err := SourceToInputWithLogger(t.Context(), tc.verifier, tc.src, tc.platform, nil)
|
inp, unknowns, err := sourceToInput(t.Context(), tc.verifier, tc.src, tc.platform, nil)
|
||||||
if tc.assert != nil {
|
if tc.assert != nil {
|
||||||
tc.assert(t, inp, unknowns, err)
|
tc.assert(t, inp, unknowns, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package sourcemeta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/moby/buildkit/client/llb/sourceresolver"
|
||||||
|
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
||||||
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ToResolverOpt(req *gwpb.ResolveSourceMetaRequest, defaultPlatform *ocispecs.Platform) sourceresolver.Opt {
|
||||||
|
platform := defaultPlatform
|
||||||
|
if req != nil && req.Platform != nil {
|
||||||
|
platform = &ocispecs.Platform{
|
||||||
|
Architecture: req.Platform.Architecture,
|
||||||
|
OS: req.Platform.OS,
|
||||||
|
Variant: req.Platform.Variant,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opt := sourceresolver.Opt{}
|
||||||
|
if req != nil {
|
||||||
|
opt.LogName = req.LogName
|
||||||
|
opt.SourcePolicies = req.SourcePolicies
|
||||||
|
}
|
||||||
|
if req != nil && req.Image != nil {
|
||||||
|
opt.ImageOpt = &sourceresolver.ResolveImageOpt{
|
||||||
|
NoConfig: req.Image.NoConfig,
|
||||||
|
AttestationChain: req.Image.AttestationChain,
|
||||||
|
ResolveAttestations: append([]string(nil), req.Image.ResolveAttestations...),
|
||||||
|
ResolveMode: req.ResolveMode,
|
||||||
|
Platform: platform,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req != nil && req.Git != nil {
|
||||||
|
opt.GitOpt = &sourceresolver.ResolveGitOpt{ReturnObject: req.Git.ReturnObject}
|
||||||
|
}
|
||||||
|
return opt
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToGatewayMetaResponse(resp *sourceresolver.MetaResponse) *gwpb.ResolveSourceMetaResponse {
|
||||||
|
out := &gwpb.ResolveSourceMetaResponse{Source: resp.Op}
|
||||||
|
if resp.Image != nil {
|
||||||
|
out.Image = &gwpb.ResolveSourceImageResponse{
|
||||||
|
Digest: resp.Image.Digest.String(),
|
||||||
|
Config: resp.Image.Config,
|
||||||
|
AttestationChain: toGatewayAttestationChain(resp.Image.AttestationChain),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if resp.Git != nil {
|
||||||
|
out.Git = &gwpb.ResolveSourceGitResponse{
|
||||||
|
Checksum: resp.Git.Checksum,
|
||||||
|
Ref: resp.Git.Ref,
|
||||||
|
CommitChecksum: resp.Git.CommitChecksum,
|
||||||
|
CommitObject: resp.Git.CommitObject,
|
||||||
|
TagObject: resp.Git.TagObject,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if resp.HTTP != nil {
|
||||||
|
var lastModified *timestamppb.Timestamp
|
||||||
|
if resp.HTTP.LastModified != nil {
|
||||||
|
lastModified = timestamppb.New(*resp.HTTP.LastModified)
|
||||||
|
}
|
||||||
|
out.HTTP = &gwpb.ResolveSourceHTTPResponse{
|
||||||
|
Checksum: resp.HTTP.Digest.String(),
|
||||||
|
Filename: resp.HTTP.Filename,
|
||||||
|
LastModified: lastModified,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func toGatewayDescriptor(desc ocispecs.Descriptor) *gwpb.Descriptor {
|
||||||
|
return &gwpb.Descriptor{
|
||||||
|
MediaType: desc.MediaType,
|
||||||
|
Digest: desc.Digest.String(),
|
||||||
|
Size: desc.Size,
|
||||||
|
Annotations: desc.Annotations,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toGatewayAttestationChain(chain *sourceresolver.AttestationChain) *gwpb.AttestationChain {
|
||||||
|
if chain == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
signatures := make([]string, 0, len(chain.SignatureManifests))
|
||||||
|
for _, dgst := range chain.SignatureManifests {
|
||||||
|
signatures = append(signatures, dgst.String())
|
||||||
|
}
|
||||||
|
blobs := make(map[string]*gwpb.Blob, len(chain.Blobs))
|
||||||
|
for dgst, blob := range chain.Blobs {
|
||||||
|
blobs[dgst.String()] = &gwpb.Blob{
|
||||||
|
Descriptor_: toGatewayDescriptor(blob.Descriptor),
|
||||||
|
Data: blob.Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &gwpb.AttestationChain{
|
||||||
|
Root: chain.Root.String(),
|
||||||
|
ImageManifest: chain.ImageManifest.String(),
|
||||||
|
AttestationManifest: chain.AttestationManifest.String(),
|
||||||
|
SignatureManifests: signatures,
|
||||||
|
Blobs: blobs,
|
||||||
|
}
|
||||||
|
}
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
package purl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containerd/platforms"
|
||||||
|
"github.com/distribution/reference"
|
||||||
|
digest "github.com/opencontainers/go-digest"
|
||||||
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
packageurl "github.com/package-url/packageurl-go"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RefToPURL converts an image reference with optional platform constraint to a package URL.
|
||||||
|
// Image references are defined in https://github.com/distribution/distribution/blob/v2.8.1/reference/reference.go#L1
|
||||||
|
// Package URLs are defined in https://github.com/package-url/purl-spec
|
||||||
|
func RefToPURL(purlType string, ref string, platform *ocispecs.Platform) (string, error) {
|
||||||
|
named, err := reference.ParseNormalizedNamed(ref)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrapf(err, "failed to parse ref %q", ref)
|
||||||
|
}
|
||||||
|
var qualifiers []packageurl.Qualifier
|
||||||
|
|
||||||
|
if canonical, ok := named.(reference.Canonical); ok {
|
||||||
|
qualifiers = append(qualifiers, packageurl.Qualifier{
|
||||||
|
Key: "digest",
|
||||||
|
Value: canonical.Digest().String(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
named = reference.TagNameOnly(named)
|
||||||
|
}
|
||||||
|
|
||||||
|
version := ""
|
||||||
|
if tagged, ok := named.(reference.Tagged); ok {
|
||||||
|
version = tagged.Tag()
|
||||||
|
}
|
||||||
|
|
||||||
|
name := reference.FamiliarName(named)
|
||||||
|
|
||||||
|
ns := ""
|
||||||
|
parts := strings.Split(name, "/")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
ns = strings.Join(parts[:len(parts)-1], "/")
|
||||||
|
}
|
||||||
|
name = parts[len(parts)-1]
|
||||||
|
|
||||||
|
if platform != nil {
|
||||||
|
p := platforms.Normalize(*platform)
|
||||||
|
qualifiers = append(qualifiers, packageurl.Qualifier{
|
||||||
|
Key: "platform",
|
||||||
|
Value: platforms.Format(p),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
p := packageurl.NewPackageURL(purlType, ns, name, version, qualifiers, "")
|
||||||
|
return p.ToString(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PURLToRef converts a package URL to an image reference and platform.
|
||||||
|
func PURLToRef(purl string) (string, *ocispecs.Platform, error) {
|
||||||
|
p, err := packageurl.FromString(purl)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if p.Type != "docker" {
|
||||||
|
return "", nil, errors.Errorf("invalid package type %q, expecting docker", p.Type)
|
||||||
|
}
|
||||||
|
ref := p.Name
|
||||||
|
if p.Namespace != "" {
|
||||||
|
ref = p.Namespace + "/" + ref
|
||||||
|
}
|
||||||
|
dgstVersion := ""
|
||||||
|
if p.Version != "" {
|
||||||
|
dgst, err := digest.Parse(p.Version)
|
||||||
|
if err == nil {
|
||||||
|
ref = ref + "@" + dgst.String()
|
||||||
|
dgstVersion = dgst.String()
|
||||||
|
} else {
|
||||||
|
ref += ":" + p.Version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var platform *ocispecs.Platform
|
||||||
|
for _, q := range p.Qualifiers {
|
||||||
|
if q.Key == "platform" {
|
||||||
|
p, err := platforms.Parse(q.Value)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// OS-version and OS-features are not included when serializing a
|
||||||
|
// platform as a string, however, containerd platforms.Parse appends
|
||||||
|
// missing information (including os-version) based on the host's
|
||||||
|
// platform.
|
||||||
|
//
|
||||||
|
// Given that this information is not obtained from the package-URL,
|
||||||
|
// we're resetting this information. Ideally, we'd do the same for
|
||||||
|
// "OS" and "architecture" (when not included in the URL).
|
||||||
|
//
|
||||||
|
// See:
|
||||||
|
// - https://github.com/containerd/containerd/commit/cfb30a31a8507e4417d42d38c9a99b04fc8af8a9 (https://github.com/containerd/containerd/pull/8778)
|
||||||
|
// - https://github.com/moby/buildkit/pull/4315#discussion_r1355141241
|
||||||
|
p.OSVersion = ""
|
||||||
|
p.OSFeatures = nil
|
||||||
|
platform = &p
|
||||||
|
}
|
||||||
|
if q.Key == "digest" {
|
||||||
|
if dgstVersion != "" {
|
||||||
|
if dgstVersion != q.Value {
|
||||||
|
return "", nil, errors.Errorf("digest %q does not match version %q", q.Value, dgstVersion)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dgst, err := digest.Parse(q.Value)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
ref = ref + "@" + dgst.String()
|
||||||
|
dgstVersion = dgst.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dgstVersion == "" && p.Version == "" {
|
||||||
|
ref += ":latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
named, err := reference.ParseNormalizedNamed(ref)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, errors.Wrapf(err, "invalid image url %q", purl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return named.String(), platform, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, build with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||||
|
.glide/
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
# individual linter configs go here
|
||||||
|
linters-settings:
|
||||||
|
|
||||||
|
# default linters are enabled `golangci-lint help linters`
|
||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- deadcode
|
||||||
|
- errcheck
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- structcheck
|
||||||
|
- typecheck
|
||||||
|
- unused
|
||||||
|
- varcheck
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
Copyright (c) the purl authors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
.PHONY: test clean lint
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -v -cover ./...
|
||||||
|
|
||||||
|
lint:
|
||||||
|
go get -u golang.org/x/lint/golint
|
||||||
|
golint -set_exit_status
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
# packageurl-go
|
||||||
|
|
||||||
|
[](https://github.com/package-url/packageurl-go/actions?query=workflow%3Atest) [](https://coveralls.io/github/package-url/packageurl-go) [](https://pkg.go.dev/github.com/package-url/packageurl-go) [](https://goreportcard.com/report/github.com/package-url/packageurl-go)
|
||||||
|
|
||||||
|
Go implementation of the package url spec.
|
||||||
|
|
||||||
|
|
||||||
|
## Install
|
||||||
|
```
|
||||||
|
go get -u github.com/package-url/packageurl-go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
The versions will follow the spec. So if the spec is released at ``1.0``. Then all versions in the ``1.x.y`` will follow the ``1.x`` spec.
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Create from parts
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/package-url/packageurl-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
instance := packageurl.NewPackageURL("test", "ok", "name", "version", nil, "")
|
||||||
|
fmt.Printf("%s", instance.ToString())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parse from string
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/package-url/packageurl-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
instance, err := packageurl.FromString("test:ok/name@version")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%#v", instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Test
|
||||||
|
Testing using the normal ``go test`` command. Using ``make test`` will pull the test fixtures shared between all package-url projects and then execute the tests.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ make test
|
||||||
|
go test -v -cover ./...
|
||||||
|
=== RUN TestFromStringExamples
|
||||||
|
--- PASS: TestFromStringExamples (0.00s)
|
||||||
|
=== RUN TestToStringExamples
|
||||||
|
--- PASS: TestToStringExamples (0.00s)
|
||||||
|
=== RUN TestStringer
|
||||||
|
--- PASS: TestStringer (0.00s)
|
||||||
|
=== RUN TestQualifiersMapConversion
|
||||||
|
--- PASS: TestQualifiersMapConversion (0.00s)
|
||||||
|
PASS
|
||||||
|
coverage: 90.7% of statements
|
||||||
|
ok github.com/package-url/packageurl-go 0.004s coverage: 90.7% of statements
|
||||||
|
```
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
0.0.0
|
||||||
+438
@@ -0,0 +1,438 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) the purl authors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package packageurl implements the package-url spec
|
||||||
|
package packageurl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// QualifierKeyPattern describes a valid qualifier key:
|
||||||
|
//
|
||||||
|
// - The key must be composed only of ASCII letters and numbers, '.',
|
||||||
|
// '-' and '_' (period, dash and underscore).
|
||||||
|
// - A key cannot start with a number.
|
||||||
|
QualifierKeyPattern = regexp.MustCompile(`^[A-Za-z\.\-_][0-9A-Za-z\.\-_]*$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// These are the known purl types as defined in the spec. Some of these require
|
||||||
|
// special treatment during parsing.
|
||||||
|
// https://github.com/package-url/purl-spec#known-purl-types
|
||||||
|
var (
|
||||||
|
// TypeBitbucket is a pkg:bitbucket purl.
|
||||||
|
TypeBitbucket = "bitbucket"
|
||||||
|
// TypeCocoapods is a pkg:cocoapods purl.
|
||||||
|
TypeCocoapods = "cocoapods"
|
||||||
|
// TypeCargo is a pkg:cargo purl.
|
||||||
|
TypeCargo = "cargo"
|
||||||
|
// TypeComposer is a pkg:composer purl.
|
||||||
|
TypeComposer = "composer"
|
||||||
|
// TypeConan is a pkg:conan purl.
|
||||||
|
TypeConan = "conan"
|
||||||
|
// TypeConda is a pkg:conda purl.
|
||||||
|
TypeConda = "conda"
|
||||||
|
// TypeCran is a pkg:cran purl.
|
||||||
|
TypeCran = "cran"
|
||||||
|
// TypeDebian is a pkg:deb purl.
|
||||||
|
TypeDebian = "deb"
|
||||||
|
// TypeDocker is a pkg:docker purl.
|
||||||
|
TypeDocker = "docker"
|
||||||
|
// TypeGem is a pkg:gem purl.
|
||||||
|
TypeGem = "gem"
|
||||||
|
// TypeGeneric is a pkg:generic purl.
|
||||||
|
TypeGeneric = "generic"
|
||||||
|
// TypeGithub is a pkg:github purl.
|
||||||
|
TypeGithub = "github"
|
||||||
|
// TypeGolang is a pkg:golang purl.
|
||||||
|
TypeGolang = "golang"
|
||||||
|
// TypeHackage is a pkg:hackage purl.
|
||||||
|
TypeHackage = "hackage"
|
||||||
|
// TypeHex is a pkg:hex purl.
|
||||||
|
TypeHex = "hex"
|
||||||
|
// TypeMaven is a pkg:maven purl.
|
||||||
|
TypeMaven = "maven"
|
||||||
|
// TypeNPM is a pkg:npm purl.
|
||||||
|
TypeNPM = "npm"
|
||||||
|
// TypeNuget is a pkg:nuget purl.
|
||||||
|
TypeNuget = "nuget"
|
||||||
|
// TypeOCI is a pkg:oci purl
|
||||||
|
TypeOCI = "oci"
|
||||||
|
// TypePyPi is a pkg:pypi purl.
|
||||||
|
TypePyPi = "pypi"
|
||||||
|
// TypeRPM is a pkg:rpm purl.
|
||||||
|
TypeRPM = "rpm"
|
||||||
|
// TypeSwift is pkg:swift purl
|
||||||
|
TypeSwift = "swift"
|
||||||
|
// TypeHuggingface is pkg:huggingface purl.
|
||||||
|
TypeHuggingface = "huggingface"
|
||||||
|
// TypeMLflow is pkg:mlflow purl.
|
||||||
|
TypeMLFlow = "mlflow"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Qualifier represents a single key=value qualifier in the package url
|
||||||
|
type Qualifier struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q Qualifier) String() string {
|
||||||
|
// A value must be a percent-encoded string
|
||||||
|
return fmt.Sprintf("%s=%s", q.Key, url.PathEscape(q.Value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qualifiers is a slice of key=value pairs, with order preserved as it appears
|
||||||
|
// in the package URL.
|
||||||
|
type Qualifiers []Qualifier
|
||||||
|
|
||||||
|
// QualifiersFromMap constructs a Qualifiers slice from a string map. To get a
|
||||||
|
// deterministic qualifier order (despite maps not providing any iteration order
|
||||||
|
// guarantees) the returned Qualifiers are sorted in increasing order of key.
|
||||||
|
func QualifiersFromMap(mm map[string]string) Qualifiers {
|
||||||
|
q := Qualifiers{}
|
||||||
|
|
||||||
|
for k, v := range mm {
|
||||||
|
q = append(q, Qualifier{Key: k, Value: v})
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort for deterministic qualifier order
|
||||||
|
sort.Slice(q, func(i int, j int) bool { return q[i].Key < q[j].Key })
|
||||||
|
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map converts a Qualifiers struct to a string map.
|
||||||
|
func (qq Qualifiers) Map() map[string]string {
|
||||||
|
m := make(map[string]string)
|
||||||
|
|
||||||
|
for i := 0; i < len(qq); i++ {
|
||||||
|
k := qq[i].Key
|
||||||
|
v := qq[i].Value
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qq Qualifiers) String() string {
|
||||||
|
var kvPairs []string
|
||||||
|
for _, q := range qq {
|
||||||
|
kvPairs = append(kvPairs, q.String())
|
||||||
|
}
|
||||||
|
return strings.Join(kvPairs, "&")
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageURL is the struct representation of the parts that make a package url
|
||||||
|
type PackageURL struct {
|
||||||
|
Type string
|
||||||
|
Namespace string
|
||||||
|
Name string
|
||||||
|
Version string
|
||||||
|
Qualifiers Qualifiers
|
||||||
|
Subpath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPackageURL creates a new PackageURL struct instance based on input
|
||||||
|
func NewPackageURL(purlType, namespace, name, version string,
|
||||||
|
qualifiers Qualifiers, subpath string) *PackageURL {
|
||||||
|
|
||||||
|
return &PackageURL{
|
||||||
|
Type: purlType,
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: name,
|
||||||
|
Version: version,
|
||||||
|
Qualifiers: qualifiers,
|
||||||
|
Subpath: subpath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToString returns the human-readable instance of the PackageURL structure.
|
||||||
|
// This is the literal purl as defined by the spec.
|
||||||
|
func (p *PackageURL) ToString() string {
|
||||||
|
// Start with the type and a colon
|
||||||
|
purl := fmt.Sprintf("pkg:%s/", p.Type)
|
||||||
|
// Add namespaces if provided
|
||||||
|
if p.Namespace != "" {
|
||||||
|
var ns []string
|
||||||
|
for _, item := range strings.Split(p.Namespace, "/") {
|
||||||
|
ns = append(ns, url.QueryEscape(item))
|
||||||
|
}
|
||||||
|
purl = purl + strings.Join(ns, "/") + "/"
|
||||||
|
}
|
||||||
|
// The name is always required and must be a percent-encoded string
|
||||||
|
// Use url.QueryEscape instead of PathEscape, as it handles @ signs
|
||||||
|
purl = purl + url.QueryEscape(p.Name)
|
||||||
|
// If a version is provided, add it after the at symbol
|
||||||
|
if p.Version != "" {
|
||||||
|
// A name must be a percent-encoded string
|
||||||
|
purl = purl + "@" + url.PathEscape(p.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over qualifiers and make groups of key=value
|
||||||
|
var qualifiers []string
|
||||||
|
for _, q := range p.Qualifiers {
|
||||||
|
qualifiers = append(qualifiers, q.String())
|
||||||
|
}
|
||||||
|
// If there are one or more key=value pairs, append on the package url
|
||||||
|
if len(qualifiers) != 0 {
|
||||||
|
purl = purl + "?" + strings.Join(qualifiers, "&")
|
||||||
|
}
|
||||||
|
// Add a subpath if available
|
||||||
|
if p.Subpath != "" {
|
||||||
|
purl = purl + "#" + p.Subpath
|
||||||
|
}
|
||||||
|
return purl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PackageURL) String() string {
|
||||||
|
return p.ToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromString parses a valid package url string into a PackageURL structure
|
||||||
|
func FromString(purl string) (PackageURL, error) {
|
||||||
|
initialIndex := strings.Index(purl, "#")
|
||||||
|
// Start with purl being stored in the remainder
|
||||||
|
remainder := purl
|
||||||
|
substring := ""
|
||||||
|
if initialIndex != -1 {
|
||||||
|
initialSplit := strings.SplitN(purl, "#", 2)
|
||||||
|
remainder = initialSplit[0]
|
||||||
|
rightSide := initialSplit[1]
|
||||||
|
rightSide = strings.TrimLeft(rightSide, "/")
|
||||||
|
rightSide = strings.TrimRight(rightSide, "/")
|
||||||
|
var rightSides []string
|
||||||
|
|
||||||
|
for _, item := range strings.Split(rightSide, "/") {
|
||||||
|
item = strings.Replace(item, ".", "", -1)
|
||||||
|
item = strings.Replace(item, "..", "", -1)
|
||||||
|
if item != "" {
|
||||||
|
i, err := url.PathUnescape(item)
|
||||||
|
if err != nil {
|
||||||
|
return PackageURL{}, fmt.Errorf("failed to unescape path: %s", err)
|
||||||
|
}
|
||||||
|
rightSides = append(rightSides, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
substring = strings.Join(rightSides, "/")
|
||||||
|
}
|
||||||
|
qualifiers := Qualifiers{}
|
||||||
|
index := strings.LastIndex(remainder, "?")
|
||||||
|
// If we don't have anything to split then return an empty result
|
||||||
|
if index != -1 {
|
||||||
|
qualifier := remainder[index+1:]
|
||||||
|
for _, item := range strings.Split(qualifier, "&") {
|
||||||
|
kv := strings.Split(item, "=")
|
||||||
|
key := strings.ToLower(kv[0])
|
||||||
|
key, err := url.PathUnescape(key)
|
||||||
|
if err != nil {
|
||||||
|
return PackageURL{}, fmt.Errorf("failed to unescape qualifier key: %s", err)
|
||||||
|
}
|
||||||
|
if !validQualifierKey(key) {
|
||||||
|
return PackageURL{}, fmt.Errorf("invalid qualifier key: '%s'", key)
|
||||||
|
}
|
||||||
|
// TODO
|
||||||
|
// - If the `key` is `checksums`, split the `value` on ',' to create
|
||||||
|
// a list of `checksums`
|
||||||
|
if kv[1] == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value, err := url.PathUnescape(kv[1])
|
||||||
|
if err != nil {
|
||||||
|
return PackageURL{}, fmt.Errorf("failed to unescape qualifier value: %s", err)
|
||||||
|
}
|
||||||
|
qualifiers = append(qualifiers, Qualifier{key, value})
|
||||||
|
}
|
||||||
|
remainder = remainder[:index]
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSplit := strings.SplitN(remainder, ":", 2)
|
||||||
|
if len(nextSplit) != 2 || nextSplit[0] != "pkg" {
|
||||||
|
return PackageURL{}, errors.New("scheme is missing")
|
||||||
|
}
|
||||||
|
// leading slashes after pkg: are to be ignored (pkg://maven is
|
||||||
|
// equivalent to pkg:maven)
|
||||||
|
remainder = strings.TrimLeft(nextSplit[1], "/")
|
||||||
|
|
||||||
|
nextSplit = strings.SplitN(remainder, "/", 2)
|
||||||
|
if len(nextSplit) != 2 {
|
||||||
|
return PackageURL{}, errors.New("type is missing")
|
||||||
|
}
|
||||||
|
// purl type is case-insensitive, canonical form is lower-case
|
||||||
|
purlType := strings.ToLower(nextSplit[0])
|
||||||
|
remainder = nextSplit[1]
|
||||||
|
|
||||||
|
index = strings.LastIndex(remainder, "/")
|
||||||
|
name := typeAdjustName(purlType, remainder[index+1:], qualifiers)
|
||||||
|
version := ""
|
||||||
|
|
||||||
|
atIndex := strings.Index(name, "@")
|
||||||
|
if atIndex != -1 {
|
||||||
|
v, err := url.PathUnescape(name[atIndex+1:])
|
||||||
|
if err != nil {
|
||||||
|
return PackageURL{}, fmt.Errorf("failed to unescape purl version: %s", err)
|
||||||
|
}
|
||||||
|
version = typeAdjustVersion(purlType, v)
|
||||||
|
|
||||||
|
unecapeName, err := url.PathUnescape(name[:atIndex])
|
||||||
|
if err != nil {
|
||||||
|
return PackageURL{}, fmt.Errorf("failed to unescape purl name: %s", err)
|
||||||
|
}
|
||||||
|
name = unecapeName
|
||||||
|
}
|
||||||
|
var namespaces []string
|
||||||
|
|
||||||
|
if index != -1 {
|
||||||
|
remainder = remainder[:index]
|
||||||
|
|
||||||
|
for _, item := range strings.Split(remainder, "/") {
|
||||||
|
if item != "" {
|
||||||
|
unescaped, err := url.PathUnescape(item)
|
||||||
|
if err != nil {
|
||||||
|
return PackageURL{}, fmt.Errorf("failed to unescape path: %s", err)
|
||||||
|
}
|
||||||
|
namespaces = append(namespaces, unescaped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
namespace := strings.Join(namespaces, "/")
|
||||||
|
namespace = typeAdjustNamespace(purlType, namespace)
|
||||||
|
|
||||||
|
// Fail if name is empty at this point
|
||||||
|
if name == "" {
|
||||||
|
return PackageURL{}, errors.New("name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := validCustomRules(purlType, name, namespace, version, qualifiers)
|
||||||
|
if err != nil {
|
||||||
|
return PackageURL{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return PackageURL{
|
||||||
|
Type: purlType,
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: name,
|
||||||
|
Version: version,
|
||||||
|
Qualifiers: qualifiers,
|
||||||
|
Subpath: substring,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make any purl type-specific adjustments to the parsed namespace.
|
||||||
|
// See https://github.com/package-url/purl-spec#known-purl-types
|
||||||
|
func typeAdjustNamespace(purlType, ns string) string {
|
||||||
|
switch purlType {
|
||||||
|
case TypeBitbucket, TypeDebian, TypeGithub, TypeGolang, TypeNPM, TypeRPM, TypeComposer:
|
||||||
|
return strings.ToLower(ns)
|
||||||
|
}
|
||||||
|
return ns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make any purl type-specific adjustments to the parsed name.
|
||||||
|
// See https://github.com/package-url/purl-spec#known-purl-types
|
||||||
|
func typeAdjustName(purlType, name string, qualifiers Qualifiers) string {
|
||||||
|
quals := qualifiers.Map()
|
||||||
|
switch purlType {
|
||||||
|
case TypeBitbucket, TypeDebian, TypeGithub, TypeGolang, TypeNPM, TypeComposer:
|
||||||
|
return strings.ToLower(name)
|
||||||
|
case TypePyPi:
|
||||||
|
return strings.ToLower(strings.ReplaceAll(name, "_", "-"))
|
||||||
|
case TypeMLFlow:
|
||||||
|
return adjustMlflowName(name, quals)
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make any purl type-specific adjustments to the parsed version.
|
||||||
|
// See https://github.com/package-url/purl-spec#known-purl-types
|
||||||
|
func typeAdjustVersion(purlType, version string) string {
|
||||||
|
switch purlType {
|
||||||
|
case TypeHuggingface:
|
||||||
|
return strings.ToLower(version)
|
||||||
|
}
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#mlflow
|
||||||
|
func adjustMlflowName(name string, qualifiers map[string]string) string {
|
||||||
|
if repo, ok := qualifiers["repository_url"]; ok {
|
||||||
|
if strings.Contains(repo, "azureml") {
|
||||||
|
// Azure ML is case-sensitive and must be kept as-is
|
||||||
|
return name
|
||||||
|
} else if strings.Contains(repo, "databricks") {
|
||||||
|
// Databricks is case-insensitive and must be lowercased
|
||||||
|
return strings.ToLower(name)
|
||||||
|
} else {
|
||||||
|
// Unknown repository type, keep as-is
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No repository qualifier given, keep as-is
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validQualifierKey validates a qualifierKey against our QualifierKeyPattern.
|
||||||
|
func validQualifierKey(key string) bool {
|
||||||
|
return QualifierKeyPattern.MatchString(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validCustomRules evaluates additional rules for each package url type, as specified in the package-url specification.
|
||||||
|
// On success, it returns nil. On failure, a descriptive error will be returned.
|
||||||
|
func validCustomRules(purlType, name, ns, version string, qualifiers Qualifiers) error {
|
||||||
|
q := qualifiers.Map()
|
||||||
|
switch purlType {
|
||||||
|
case TypeConan:
|
||||||
|
if ns != "" {
|
||||||
|
if val, ok := q["channel"]; ok {
|
||||||
|
if val == "" {
|
||||||
|
return errors.New("the qualifier channel must be not empty if namespace is present")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return errors.New("channel qualifier does not exist")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if val, ok := q["channel"]; ok {
|
||||||
|
if val != "" {
|
||||||
|
return errors.New("namespace is required if channel is non empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case TypeSwift:
|
||||||
|
if ns == "" {
|
||||||
|
return errors.New("namespace is required")
|
||||||
|
}
|
||||||
|
if version == "" {
|
||||||
|
return errors.New("version is required")
|
||||||
|
}
|
||||||
|
case TypeCran:
|
||||||
|
if version == "" {
|
||||||
|
return errors.New("version is required")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Vendored
+4
@@ -730,6 +730,7 @@ github.com/moby/buildkit/util/pgpsign
|
|||||||
github.com/moby/buildkit/util/progress
|
github.com/moby/buildkit/util/progress
|
||||||
github.com/moby/buildkit/util/progress/progressui
|
github.com/moby/buildkit/util/progress/progressui
|
||||||
github.com/moby/buildkit/util/progress/progresswriter
|
github.com/moby/buildkit/util/progress/progresswriter
|
||||||
|
github.com/moby/buildkit/util/purl
|
||||||
github.com/moby/buildkit/util/resolver/config
|
github.com/moby/buildkit/util/resolver/config
|
||||||
github.com/moby/buildkit/util/resolver/limited
|
github.com/moby/buildkit/util/resolver/limited
|
||||||
github.com/moby/buildkit/util/resolver/retryhandler
|
github.com/moby/buildkit/util/resolver/retryhandler
|
||||||
@@ -930,6 +931,9 @@ github.com/opencontainers/go-digest
|
|||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
github.com/opencontainers/image-spec/specs-go
|
github.com/opencontainers/image-spec/specs-go
|
||||||
github.com/opencontainers/image-spec/specs-go/v1
|
github.com/opencontainers/image-spec/specs-go/v1
|
||||||
|
# github.com/package-url/packageurl-go v0.1.1
|
||||||
|
## explicit; go 1.17
|
||||||
|
github.com/package-url/packageurl-go
|
||||||
# github.com/pelletier/go-toml/v2 v2.2.4
|
# github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
## explicit; go 1.21.0
|
## explicit; go 1.21.0
|
||||||
github.com/pelletier/go-toml/v2
|
github.com/pelletier/go-toml/v2
|
||||||
|
|||||||
Reference in New Issue
Block a user