Add proper mirror support (#74)
* Add rewrite support and fix existing tests * Add unit tests for policy matching * Compile regexes up front and store policies in map * Add test for verify flow with mirror * Rename ImageName -> ResolvedName And only set it when necessary * Rename Rewrite -> Replacement but keep it as rewrite in the yaml
This commit is contained in:
@@ -22,12 +22,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||||
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
|
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
|
||||||
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
|
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
|
||||||
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
|
PassMirrorPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-mirror")
|
||||||
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
|
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
|
||||||
TestTempDir = "attest-sign-test"
|
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
|
||||||
|
TestTempDir = "attest-sign-test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSignVerifyOCILayout(t *testing.T) {
|
func TestSignVerifyOCILayout(t *testing.T) {
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package attest
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/attest/pkg/attestation"
|
"github.com/docker/attest/pkg/attestation"
|
||||||
"github.com/docker/attest/pkg/config"
|
"github.com/docker/attest/pkg/config"
|
||||||
"github.com/docker/attest/pkg/oci"
|
"github.com/docker/attest/pkg/oci"
|
||||||
@@ -36,12 +38,12 @@ func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.PolicyOptions)
|
|||||||
}
|
}
|
||||||
// this is overriding the mapping with a referrers config. Useful for testing if nothing else
|
// this is overriding the mapping with a referrers config. Useful for testing if nothing else
|
||||||
if opts.ReferrersRepo != "" {
|
if opts.ReferrersRepo != "" {
|
||||||
pctx.Mapping.Attestations = &config.ReferrersConfig{
|
pctx.Mapping.Attestations = &config.AttestationConfig{
|
||||||
Repo: opts.ReferrersRepo,
|
Repo: opts.ReferrersRepo,
|
||||||
Style: config.AttestationStyleReferrers,
|
Style: config.AttestationStyleReferrers,
|
||||||
}
|
}
|
||||||
} else if opts.AttestationStyle == config.AttestationStyleAttached {
|
} else if opts.AttestationStyle == config.AttestationStyleAttached {
|
||||||
pctx.Mapping.Attestations = &config.ReferrersConfig{
|
pctx.Mapping.Attestations = &config.AttestationConfig{
|
||||||
Repo: opts.ReferrersRepo,
|
Repo: opts.ReferrersRepo,
|
||||||
Style: config.AttestationStyleAttached,
|
Style: config.AttestationStyleAttached,
|
||||||
}
|
}
|
||||||
@@ -122,6 +124,19 @@ func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, p
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pctx.ResolvedName != "" {
|
||||||
|
// this means the name we have is not the one we want to use for policy evaluation
|
||||||
|
// so we need to replace it with the one we resolved during policy resolution.
|
||||||
|
// this can happen if the name is an alias for another image, e.g. if it is a mirror
|
||||||
|
ref, err := reference.ParseNormalizedNamed(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse image name: %w", err)
|
||||||
|
}
|
||||||
|
oldName := ref.Name()
|
||||||
|
name = strings.Replace(name, oldName, pctx.ResolvedName, 1)
|
||||||
|
}
|
||||||
|
|
||||||
purl, canonical, err := oci.RefToPURL(name, platform)
|
purl, canonical, err := oci.RefToPURL(name, platform)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to convert ref to purl: %w", err)
|
return nil, fmt.Errorf("failed to convert ref to purl: %w", err)
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func TestVerifyAttestations(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx := policy.WithPolicyEvaluator(context.Background(), &mockPE)
|
ctx := policy.WithPolicyEvaluator(context.Background(), &mockPE)
|
||||||
_, err := VerifyAttestations(ctx, resolver, nil)
|
_, err := VerifyAttestations(ctx, resolver, &policy.Policy{ResolvedName: ""})
|
||||||
if tc.expectedError != nil {
|
if tc.expectedError != nil {
|
||||||
if assert.Error(t, err) {
|
if assert.Error(t, err) {
|
||||||
assert.Equal(t, tc.expectedError.Error(), err.Error())
|
assert.Equal(t, tc.expectedError.Error(), err.Error())
|
||||||
@@ -189,22 +189,24 @@ func TestVerificationFailure(t *testing.T) {
|
|||||||
assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI)
|
assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI)
|
||||||
}
|
}
|
||||||
|
|
||||||
// test signing without a TL entry
|
func TestSignVerify(t *testing.T) {
|
||||||
func TestSignVerifyNoTL(t *testing.T) {
|
|
||||||
ctx, signer := test.Setup(t)
|
ctx, signer := test.Setup(t)
|
||||||
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
|
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
|
||||||
// setup an image with signed attestations
|
// setup an image with signed attestations
|
||||||
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
signTL bool
|
signTL bool
|
||||||
policyDir string
|
policyDir string
|
||||||
success bool
|
imageName string
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir, success: true},
|
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir},
|
||||||
{name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir, success: false},
|
{name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir},
|
||||||
{name: "no tl", signTL: false, policyDir: PassPolicyDir, success: false},
|
{name: "no tl", signTL: false, policyDir: PassPolicyDir},
|
||||||
|
{name: "mirror", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "mirror.org/library/test-image:test"},
|
||||||
|
{name: "mirror no match", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectError: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||||
@@ -222,13 +224,18 @@ func TestSignVerifyNoTL(t *testing.T) {
|
|||||||
signedIndex := attIdx.Index
|
signedIndex := attIdx.Index
|
||||||
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
|
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
imageName := tc.imageName
|
||||||
|
if imageName == "" {
|
||||||
|
imageName = attIdx.Name
|
||||||
|
}
|
||||||
// output signed attestations
|
// output signed attestations
|
||||||
idx := v1.ImageIndex(empty.Index)
|
idx := v1.ImageIndex(empty.Index)
|
||||||
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
||||||
Add: signedIndex,
|
Add: signedIndex,
|
||||||
Descriptor: v1.Descriptor{
|
Descriptor: v1.Descriptor{
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
oci.OciReferenceTarget: attIdx.Name,
|
oci.OciReferenceTarget: imageName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -241,8 +248,17 @@ func TestSignVerifyNoTL(t *testing.T) {
|
|||||||
src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64))
|
src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
results, err := Verify(ctx, src, policyOpts)
|
results, err := Verify(ctx, src, policyOpts)
|
||||||
|
if tc.expectError {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
||||||
|
platform, err := oci.ParsePlatform(LinuxAMD64)
|
||||||
|
require.NoError(t, err)
|
||||||
|
expectedPURL, _, err := oci.RefToPURL(attIdx.Name, platform)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedPURL, results.Input.Purl)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
|
|||||||
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
|
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
|
||||||
platforms := []string{"linux/amd64", "linux/arm64"}
|
platforms := []string{"linux/amd64", "linux/arm64"}
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
server *httptest.Server
|
server *httptest.Server
|
||||||
referrersServer *httptest.Server
|
referrersServer *httptest.Server
|
||||||
skipSubject bool
|
skipSubject bool
|
||||||
@@ -44,46 +45,53 @@ func TestAttestationReferenceTypes(t *testing.T) {
|
|||||||
referrersRepo string
|
referrersRepo string
|
||||||
attestationSource config.AttestationStyle
|
attestationSource config.AttestationStyle
|
||||||
expectFailure bool
|
expectFailure bool
|
||||||
policyDir string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
name: "referrers support, defaults",
|
||||||
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "no referrers support",
|
||||||
server: httptest.NewServer(registry.New()),
|
server: httptest.NewServer(registry.New()),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "attached attestations",
|
||||||
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
skipSubject: true,
|
skipSubject: true,
|
||||||
attestationSource: config.AttestationStyleAttached,
|
attestationSource: config.AttestationStyleAttached,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "use digest",
|
||||||
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
useDigest: true,
|
useDigest: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "attached attestations, referrers repo (mismatched args)",
|
||||||
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
expectFailure: true, //mismatched args
|
expectFailure: true, //mismatched args
|
||||||
attestationSource: config.AttestationStyleAttached,
|
attestationSource: config.AttestationStyleAttached,
|
||||||
referrersRepo: "referrers",
|
referrersRepo: "referrers",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "referrers attestations, referrers repo (no policy)",
|
||||||
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
expectFailure: true, // no policy
|
expectFailure: true, // no policy
|
||||||
attestationSource: config.AttestationStyleReferrers,
|
attestationSource: config.AttestationStyleReferrers,
|
||||||
referrersRepo: "referrers",
|
referrersRepo: "referrers",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "referrers attestations",
|
||||||
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
attestationSource: config.AttestationStyleReferrers,
|
attestationSource: config.AttestationStyleReferrers,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "referrers attestations, no referrers support on server",
|
||||||
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(false))),
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(false))),
|
||||||
attestationSource: config.AttestationStyleReferrers,
|
attestationSource: config.AttestationStyleReferrers,
|
||||||
referrersServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
referrersServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(fmt.Sprint(tc), func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
s := tc.server
|
s := tc.server
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
@@ -111,6 +119,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
|
|||||||
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
|
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
for _, img := range signedManifests {
|
for _, img := range signedManifests {
|
||||||
err = mirror.PushImageToRegistry(img.Attestation.Image, fmt.Sprintf("%s:tag-does-not-matter", repo))
|
err = mirror.PushImageToRegistry(img.Attestation.Image, fmt.Sprintf("%s:tag-does-not-matter", repo))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -142,9 +151,6 @@ func TestAttestationReferenceTypes(t *testing.T) {
|
|||||||
policyOpts := &policy.PolicyOptions{
|
policyOpts := &policy.PolicyOptions{
|
||||||
LocalPolicyDir: LocalPolicy,
|
LocalPolicyDir: LocalPolicy,
|
||||||
}
|
}
|
||||||
if tc.policyDir != "" {
|
|
||||||
policyOpts.LocalPolicyDir = tc.policyDir
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.referrersRepo != "" {
|
if tc.referrersRepo != "" {
|
||||||
policyOpts.ReferrersRepo = tc.referrersRepo
|
policyOpts.ReferrersRepo = tc.referrersRepo
|
||||||
@@ -192,65 +198,70 @@ func TestReferencesInDifferentRepo(t *testing.T) {
|
|||||||
ctx, signer := test.Setup(t)
|
ctx, signer := test.Setup(t)
|
||||||
repoName := "repo"
|
repoName := "repo"
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
server *httptest.Server
|
server *httptest.Server
|
||||||
refServer *httptest.Server
|
refServer *httptest.Server
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
name: "referrers support",
|
||||||
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
refServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
refServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "no referrers support",
|
||||||
server: httptest.NewServer(registry.New()),
|
server: httptest.NewServer(registry.New()),
|
||||||
refServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
refServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
server := tc.server
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
defer server.Close()
|
server := tc.server
|
||||||
serverUrl, err := url.Parse(server.URL)
|
defer server.Close()
|
||||||
require.NoError(t, err)
|
serverUrl, err := url.Parse(server.URL)
|
||||||
|
|
||||||
refServer := tc.refServer
|
|
||||||
defer refServer.Close()
|
|
||||||
refServerUrl, err := url.Parse(refServer.URL)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
opts := &attestation.SigningOptions{
|
|
||||||
Replace: true,
|
|
||||||
SkipTL: true,
|
|
||||||
}
|
|
||||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
indexName := fmt.Sprintf("%s/%s:latest", serverUrl.Host, repoName)
|
|
||||||
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// push signed attestation image to the ref server
|
|
||||||
for _, img := range signedManifests {
|
|
||||||
// push references using subject-digest.att convention
|
|
||||||
err = mirror.PushImageToRegistry(img.Attestation.Image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName))
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
|
||||||
mfs2, err := attIdx.Index.IndexManifest()
|
refServer := tc.refServer
|
||||||
require.NoError(t, err)
|
defer refServer.Close()
|
||||||
for _, mf := range mfs2.Manifests {
|
refServerUrl, err := url.Parse(refServer.URL)
|
||||||
//skip signed/unsigned attestations
|
require.NoError(t, err)
|
||||||
if mf.Annotations[attestation.DockerReferenceType] == attestation.AttestationManifestType {
|
|
||||||
continue
|
opts := &attestation.SigningOptions{
|
||||||
|
Replace: true,
|
||||||
|
SkipTL: true,
|
||||||
}
|
}
|
||||||
// can evaluate policy using referrers in a different repo
|
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||||
referencedImage := fmt.Sprintf("%s@%s", indexName, mf.Digest.String())
|
require.NoError(t, err)
|
||||||
policyOpts := &policy.PolicyOptions{
|
|
||||||
LocalPolicyDir: PassPolicyDir,
|
indexName := fmt.Sprintf("%s/%s:latest", serverUrl.Host, repoName)
|
||||||
|
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// push signed attestation image to the ref server
|
||||||
|
for _, img := range signedManifests {
|
||||||
|
// push references using subject-digest.att convention
|
||||||
|
err = mirror.PushImageToRegistry(img.Attestation.Image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName))
|
||||||
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
src, err := oci.ParseImageSpec(referencedImage)
|
mfs2, err := attIdx.Index.IndexManifest()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
results, err := attest.Verify(ctx, src, policyOpts)
|
for _, mf := range mfs2.Manifests {
|
||||||
require.NoError(t, err)
|
//skip signed/unsigned attestations
|
||||||
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
|
if mf.Annotations[attestation.DockerReferenceType] == attestation.AttestationManifestType {
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
|
// can evaluate policy using referrers in a different repo
|
||||||
|
referencedImage := fmt.Sprintf("%s@%s", indexName, mf.Digest.String())
|
||||||
|
policyOpts := &policy.PolicyOptions{
|
||||||
|
LocalPolicyDir: PassPolicyDir,
|
||||||
|
}
|
||||||
|
src, err := oci.ParseImageSpec(referencedImage)
|
||||||
|
require.NoError(t, err)
|
||||||
|
results, err := attest.Verify(ctx, src, policyOpts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
"github.com/docker/attest/pkg/tuf"
|
"github.com/docker/attest/pkg/tuf"
|
||||||
goyaml "gopkg.in/yaml.v3"
|
goyaml "gopkg.in/yaml.v3"
|
||||||
@@ -17,7 +18,7 @@ func LoadLocalMappings(configDir string) (*PolicyMappings, error) {
|
|||||||
if configDir == "" {
|
if configDir == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
mappings := &PolicyMappings{}
|
mappings := &policyMappingsFile{}
|
||||||
path := filepath.Join(configDir, MappingFilename)
|
path := filepath.Join(configDir, MappingFilename)
|
||||||
mappingFile, err := os.ReadFile(path)
|
mappingFile, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -27,7 +28,7 @@ func LoadLocalMappings(configDir string) (*PolicyMappings, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", path, err)
|
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", path, err)
|
||||||
}
|
}
|
||||||
return mappings, nil
|
return expandMappingFile(mappings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMappings, error) {
|
func LoadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMappings, error) {
|
||||||
@@ -39,11 +40,38 @@ func LoadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to download policy mapping file %s: %w", filename, err)
|
return nil, fmt.Errorf("failed to download policy mapping file %s: %w", filename, err)
|
||||||
}
|
}
|
||||||
mappings := &PolicyMappings{}
|
mappings := &policyMappingsFile{}
|
||||||
|
|
||||||
err = goyaml.Unmarshal(fileContents, mappings)
|
err = goyaml.Unmarshal(fileContents, mappings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", filename, err)
|
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", filename, err)
|
||||||
}
|
}
|
||||||
return mappings, nil
|
return expandMappingFile(mappings)
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandMappingFile(mappingFile *policyMappingsFile) (*PolicyMappings, error) {
|
||||||
|
policies := make(map[string]*PolicyMapping)
|
||||||
|
for _, policy := range mappingFile.Policies {
|
||||||
|
policies[policy.Id] = policy
|
||||||
|
}
|
||||||
|
|
||||||
|
var rules []*PolicyRule
|
||||||
|
for _, rule := range mappingFile.Rules {
|
||||||
|
r, err := regexp.Compile(rule.Pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rules = append(rules, &PolicyRule{
|
||||||
|
Pattern: r,
|
||||||
|
PolicyId: rule.PolicyId,
|
||||||
|
Replacement: rule.Replacement,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PolicyMappings{
|
||||||
|
Version: mappingFile.Version,
|
||||||
|
Kind: mappingFile.Kind,
|
||||||
|
Policies: policies,
|
||||||
|
Rules: rules,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,25 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
type policyMappingsFile struct {
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
Kind string `yaml:"kind"`
|
||||||
|
Policies []*PolicyMapping `yaml:"policies"`
|
||||||
|
Rules []*policyRuleFile `yaml:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type policyRuleFile struct {
|
||||||
|
Pattern string `yaml:"pattern"`
|
||||||
|
PolicyId string `yaml:"policy-id"`
|
||||||
|
Replacement string `yaml:"rewrite"`
|
||||||
|
}
|
||||||
|
|
||||||
type PolicyMappings struct {
|
type PolicyMappings struct {
|
||||||
Version string `json:"version"`
|
Version string
|
||||||
Kind string `json:"kind"`
|
Kind string
|
||||||
Policies []*PolicyMapping `json:"policies"`
|
Policies map[string]*PolicyMapping
|
||||||
Mirrors []*PolicyMirror `json:"mirrors"`
|
Rules []*PolicyRule
|
||||||
}
|
}
|
||||||
|
|
||||||
type AttestationStyle string
|
type AttestationStyle string
|
||||||
@@ -15,34 +30,23 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PolicyMapping struct {
|
type PolicyMapping struct {
|
||||||
Id string `json:"id"`
|
Id string `yaml:"id"`
|
||||||
Description string `json:"description"`
|
Description string `yaml:"description"`
|
||||||
Origin *PolicyOrigin `json:"origin"`
|
Files []PolicyMappingFile `yaml:"files"`
|
||||||
Files []PolicyMappingFile `json:"files"`
|
Attestations *AttestationConfig `yaml:"attestations"`
|
||||||
Attestations *ReferrersConfig `json:"attestations"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReferrersConfig struct {
|
type AttestationConfig struct {
|
||||||
Style AttestationStyle `json:"style"`
|
Style AttestationStyle `yaml:"style"`
|
||||||
Repo string `json:"repo"`
|
Repo string `yaml:"repo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PolicyMappingFile struct {
|
type PolicyMappingFile struct {
|
||||||
Path string `json:"path"`
|
Path string `yaml:"path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PolicyMirror struct {
|
type PolicyRule struct {
|
||||||
PolicyId string `yaml:"policy-id"`
|
Pattern *regexp.Regexp
|
||||||
Mirror MirrorSpec `json:"mirror"`
|
PolicyId string
|
||||||
}
|
Replacement string
|
||||||
|
|
||||||
type MirrorSpec struct {
|
|
||||||
Domains []string `json:"domains"`
|
|
||||||
Prefix string `json:"prefix"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyOrigin struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Prefix string `json:"prefix"`
|
|
||||||
Domain string `json:"domain"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,13 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/attest/pkg/config"
|
"github.com/docker/attest/pkg/config"
|
||||||
"github.com/docker/attest/pkg/oci"
|
"github.com/docker/attest/pkg/oci"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resolveLocalPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Policy, error) {
|
func resolveLocalPolicy(opts *PolicyOptions, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
|
||||||
if opts.LocalPolicyDir == "" {
|
if opts.LocalPolicyDir == "" {
|
||||||
return nil, fmt.Errorf("local policy dir not set")
|
return nil, fmt.Errorf("local policy dir not set")
|
||||||
}
|
}
|
||||||
@@ -35,10 +33,13 @@ func resolveLocalPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Po
|
|||||||
InputFiles: files,
|
InputFiles: files,
|
||||||
Mapping: mapping,
|
Mapping: mapping,
|
||||||
}
|
}
|
||||||
|
if imageName != matchedName {
|
||||||
|
policy.ResolvedName = matchedName
|
||||||
|
}
|
||||||
return policy, nil
|
return policy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveTufPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Policy, error) {
|
func resolveTufPolicy(opts *PolicyOptions, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
|
||||||
files := make([]*PolicyFile, 0, len(mapping.Files))
|
files := make([]*PolicyFile, 0, len(mapping.Files))
|
||||||
for _, f := range mapping.Files {
|
for _, f := range mapping.Files {
|
||||||
filename := f.Path
|
filename := f.Path
|
||||||
@@ -55,32 +56,68 @@ func resolveTufPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Poli
|
|||||||
InputFiles: files,
|
InputFiles: files,
|
||||||
Mapping: mapping,
|
Mapping: mapping,
|
||||||
}
|
}
|
||||||
|
if imageName != matchedName {
|
||||||
|
policy.ResolvedName = matchedName
|
||||||
|
}
|
||||||
return policy, nil
|
return policy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findPolicyMatch(named reference.Named, mappings *config.PolicyMappings) (*config.PolicyMapping, *config.PolicyMirror) {
|
type matchType string
|
||||||
if mappings != nil {
|
|
||||||
for _, mapping := range mappings.Policies {
|
const (
|
||||||
if mapping.Origin.Domain == reference.Domain(named) &&
|
matchTypePolicy matchType = "policy"
|
||||||
strings.HasPrefix(reference.Path(named), mapping.Origin.Prefix) {
|
matchTypeMatchNoPolicy matchType = "match_no_policy"
|
||||||
return mapping, nil
|
matchTypeNoMatch matchType = "no_match"
|
||||||
}
|
)
|
||||||
}
|
|
||||||
// now search mirrors
|
type policyMatch struct {
|
||||||
for _, mirror := range mappings.Mirrors {
|
matchType matchType
|
||||||
if (slices.Contains(mirror.Mirror.Domains, reference.Domain(named)) ||
|
policy *config.PolicyMapping
|
||||||
slices.Contains(mirror.Mirror.Domains, "*")) &&
|
rule *config.PolicyRule
|
||||||
strings.HasPrefix(reference.Path(named), mirror.Mirror.Prefix) {
|
matchedName string
|
||||||
for _, mapping := range mappings.Policies {
|
}
|
||||||
if mapping.Id == mirror.PolicyId {
|
|
||||||
return mapping, nil
|
func findPolicyMatch(imageName string, mappings *config.PolicyMappings) (*policyMatch, error) {
|
||||||
}
|
if mappings == nil {
|
||||||
|
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
|
||||||
|
}
|
||||||
|
return findPolicyMatchImpl(imageName, mappings, make(map[*config.PolicyRule]bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPolicyMatchImpl(imageName string, mappings *config.PolicyMappings, matched map[*config.PolicyRule]bool) (*policyMatch, error) {
|
||||||
|
for _, rule := range mappings.Rules {
|
||||||
|
if rule.Pattern.MatchString(imageName) {
|
||||||
|
switch {
|
||||||
|
case rule.PolicyId == "" && rule.Replacement == "":
|
||||||
|
return nil, fmt.Errorf("rule %s has neither policy-id nor rewrite", rule.Pattern)
|
||||||
|
case rule.PolicyId != "" && rule.Replacement != "":
|
||||||
|
return nil, fmt.Errorf("rule %s has both policy-id and rewrite", rule.Pattern)
|
||||||
|
case rule.PolicyId != "":
|
||||||
|
policy := mappings.Policies[rule.PolicyId]
|
||||||
|
if policy != nil {
|
||||||
|
return &policyMatch{
|
||||||
|
matchType: matchTypePolicy,
|
||||||
|
policy: policy,
|
||||||
|
rule: rule,
|
||||||
|
matchedName: imageName,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
return nil, mirror
|
return &policyMatch{
|
||||||
|
matchType: matchTypeMatchNoPolicy,
|
||||||
|
rule: rule,
|
||||||
|
matchedName: imageName,
|
||||||
|
}, nil
|
||||||
|
case rule.Replacement != "":
|
||||||
|
if matched[rule] {
|
||||||
|
return nil, fmt.Errorf("rewrite loop detected")
|
||||||
|
}
|
||||||
|
matched[rule] = true
|
||||||
|
imageName = rule.Pattern.ReplaceAllString(imageName, rule.Replacement)
|
||||||
|
return findPolicyMatchImpl(imageName, mappings, matched)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, nil
|
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
|
func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
|
||||||
@@ -90,10 +127,9 @@ func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
|
|||||||
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
||||||
}
|
}
|
||||||
if localMappings != nil {
|
if localMappings != nil {
|
||||||
for _, mapping := range localMappings.Policies {
|
policy := localMappings.Policies[opts.PolicyId]
|
||||||
if mapping.Id == opts.PolicyId {
|
if policy != nil {
|
||||||
return resolveLocalPolicy(opts, mapping)
|
return resolveLocalPolicy(opts, policy, "", "")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,10 +138,9 @@ func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err)
|
return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err)
|
||||||
}
|
}
|
||||||
for _, mapping := range tufMappings.Policies {
|
policy := tufMappings.Policies[opts.PolicyId]
|
||||||
if mapping.Id == opts.PolicyId {
|
if policy != nil {
|
||||||
return resolveTufPolicy(opts, mapping)
|
return resolveTufPolicy(opts, policy, "", "")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("policy with id %s not found", opts.PolicyId)
|
return nil, fmt.Errorf("policy with id %s not found", opts.PolicyId)
|
||||||
}
|
}
|
||||||
@@ -124,7 +159,7 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get image name: %w", err)
|
return nil, fmt.Errorf("failed to get image name: %w", err)
|
||||||
}
|
}
|
||||||
named, err := reference.ParseNormalizedNamed(imageName)
|
imageName, err = normalizeImageName(imageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse image name: %w", err)
|
return nil, fmt.Errorf("failed to parse image name: %w", err)
|
||||||
}
|
}
|
||||||
@@ -132,9 +167,12 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
||||||
}
|
}
|
||||||
mapping, mirror := findPolicyMatch(named, localMappings)
|
match, err := findPolicyMatch(imageName, localMappings)
|
||||||
if mapping != nil {
|
if err != nil {
|
||||||
return resolveLocalPolicy(opts, mapping)
|
return nil, err
|
||||||
|
}
|
||||||
|
if match.matchType == matchTypePolicy {
|
||||||
|
return resolveLocalPolicy(opts, match.policy, imageName, match.matchedName)
|
||||||
}
|
}
|
||||||
// must check tuf
|
// must check tuf
|
||||||
tufMappings, err := config.LoadTufMappings(opts.TufClient, opts.LocalTargetsDir)
|
tufMappings, err := config.LoadTufMappings(opts.TufClient, opts.LocalTargetsDir)
|
||||||
@@ -143,20 +181,31 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
|
|||||||
}
|
}
|
||||||
|
|
||||||
// it's a mirror of a tuf policy
|
// it's a mirror of a tuf policy
|
||||||
if mirror != nil {
|
if match.matchType == matchTypeMatchNoPolicy {
|
||||||
for _, mapping := range tufMappings.Policies {
|
for _, mapping := range tufMappings.Policies {
|
||||||
if mapping.Id == mirror.PolicyId {
|
if mapping.Id == match.rule.PolicyId {
|
||||||
return resolveTufPolicy(opts, mapping)
|
return resolveTufPolicy(opts, mapping, imageName, match.matchedName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to resolve a tuf policy directly
|
// try to resolve a tuf policy directly
|
||||||
mapping, _ = findPolicyMatch(named, tufMappings)
|
match, err = findPolicyMatch(imageName, tufMappings)
|
||||||
if mapping == nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, err
|
||||||
}
|
}
|
||||||
return resolveTufPolicy(opts, mapping)
|
if match.matchType == matchTypePolicy {
|
||||||
|
return resolveTufPolicy(opts, match.policy, imageName, match.matchedName)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeImageName(imageName string) (string, error) {
|
||||||
|
named, err := reference.ParseNormalizedNamed(imageName)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to parse image name: %w", err)
|
||||||
|
}
|
||||||
|
return named.Name(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsResolver, error) {
|
func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsResolver, error) {
|
||||||
|
|||||||
121
pkg/policy/policy_match_test.go
Normal file
121
pkg/policy/policy_match_test.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/attest/pkg/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFindPolicyMatch(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
imageName string
|
||||||
|
mappingDir string
|
||||||
|
|
||||||
|
expectError bool
|
||||||
|
expectedMatchType matchType
|
||||||
|
expectedPolicyID string
|
||||||
|
expectedImageName string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "alpine",
|
||||||
|
mappingDir: "doi",
|
||||||
|
imageName: "docker.io/library/alpine",
|
||||||
|
|
||||||
|
expectedMatchType: matchTypePolicy,
|
||||||
|
expectedPolicyID: "docker-official-images",
|
||||||
|
expectedImageName: "docker.io/library/alpine",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no match",
|
||||||
|
mappingDir: "doi",
|
||||||
|
imageName: "docker.io/something/else",
|
||||||
|
|
||||||
|
expectedMatchType: matchTypeNoMatch,
|
||||||
|
expectedImageName: "docker.io/something/else",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match, no policy",
|
||||||
|
mappingDir: "local",
|
||||||
|
imageName: "docker.io/library/alpine",
|
||||||
|
|
||||||
|
expectedMatchType: matchTypeMatchNoPolicy,
|
||||||
|
expectedImageName: "docker.io/library/alpine",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple rewrite",
|
||||||
|
mappingDir: "simple-rewrite",
|
||||||
|
imageName: "mycoolmirror.org/library/alpine",
|
||||||
|
|
||||||
|
expectedMatchType: matchTypePolicy,
|
||||||
|
expectedPolicyID: "docker-official-images",
|
||||||
|
expectedImageName: "docker.io/library/alpine",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rewrite no match",
|
||||||
|
mappingDir: "rewrite-to-no-match",
|
||||||
|
imageName: "mycoolmirror.org/library/alpine",
|
||||||
|
|
||||||
|
expectedMatchType: matchTypeNoMatch,
|
||||||
|
expectedImageName: "badredirect.org/alpine",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rewrite to match, no policy",
|
||||||
|
mappingDir: "rewrite-to-local",
|
||||||
|
imageName: "mycoolmirror.org/library/alpine",
|
||||||
|
|
||||||
|
expectedMatchType: matchTypeMatchNoPolicy,
|
||||||
|
expectedImageName: "docker.io/library/alpine",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple rewrites",
|
||||||
|
mappingDir: "rewrite-multiple",
|
||||||
|
imageName: "myevencoolermirror.org/library/alpine",
|
||||||
|
|
||||||
|
expectedMatchType: matchTypePolicy,
|
||||||
|
expectedPolicyID: "docker-official-images",
|
||||||
|
expectedImageName: "docker.io/library/alpine",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid rewrites",
|
||||||
|
mappingDir: "rewrite-invalid",
|
||||||
|
imageName: "mycoolmirror.org/library/alpine",
|
||||||
|
|
||||||
|
expectError: true,
|
||||||
|
expectedMatchType: matchTypePolicy,
|
||||||
|
expectedPolicyID: "docker-official-images",
|
||||||
|
expectedImageName: "docker.io/library/alpine",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rewrite loop",
|
||||||
|
mappingDir: "rewrite-loop",
|
||||||
|
imageName: "yin/alpine",
|
||||||
|
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
mappings, err := config.LoadLocalMappings(filepath.Join("testdata", "mappings", tc.mappingDir))
|
||||||
|
require.NoError(t, err)
|
||||||
|
match, err := findPolicyMatch(tc.imageName, mappings)
|
||||||
|
if tc.expectError {
|
||||||
|
require.Error(t, err)
|
||||||
|
// TODO: check error matches expected error message
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.expectedMatchType, match.matchType)
|
||||||
|
if match.matchType == matchTypePolicy {
|
||||||
|
if assert.NotNil(t, match.policy) {
|
||||||
|
assert.Equal(t, tc.expectedPolicyID, match.policy.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.expectedImageName, match.matchedName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,6 +84,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
|||||||
src, err := oci.ParseImageSpec(imageName, oci.WithPlatform(platform.String()))
|
src, err := oci.ParseImageSpec(imageName, oci.WithPlatform(platform.String()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
resolver, err := policy.CreateImageDetailsResolver(src)
|
resolver, err := policy.CreateImageDetailsResolver(src)
|
||||||
|
require.NoError(t, err)
|
||||||
policy, err := policy.ResolvePolicy(ctx, resolver, tc.policy)
|
policy, err := policy.ResolvePolicy(ctx, resolver, tc.policy)
|
||||||
if tc.errorStr != "" {
|
if tc.errorStr != "" {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -91,6 +92,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
require.NoErrorf(t, err, "failed to resolve policy")
|
require.NoErrorf(t, err, "failed to resolve policy")
|
||||||
|
require.NotNil(t, policy, "policy should not be nil")
|
||||||
result, err := re.Evaluate(ctx, tc.resolver, policy, input)
|
result, err := re.Evaluate(ctx, tc.resolver, policy, input)
|
||||||
require.NoErrorf(t, err, "Evaluate failed")
|
require.NoErrorf(t, err, "Evaluate failed")
|
||||||
|
|
||||||
@@ -107,8 +109,10 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
|||||||
func TestLoadingMappings(t *testing.T) {
|
func TestLoadingMappings(t *testing.T) {
|
||||||
policyMappings, err := config.LoadLocalMappings(filepath.Join("testdata", "mock-tuf-allow"))
|
policyMappings, err := config.LoadLocalMappings(filepath.Join("testdata", "mock-tuf-allow"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, len(policyMappings.Mirrors), 1)
|
assert.Equal(t, len(policyMappings.Rules), 3)
|
||||||
for _, mirror := range policyMappings.Mirrors {
|
for _, mirror := range policyMappings.Rules {
|
||||||
assert.Equal(t, "docker-official-images", mirror.PolicyId)
|
if mirror.PolicyId != "" {
|
||||||
|
assert.Equal(t, "docker-official-images", mirror.PolicyId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
pkg/policy/testdata/mappings/doi/mapping.yaml
vendored
Normal file
10
pkg/policy/testdata/mappings/doi/mapping.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
version: v1
|
||||||
|
kind: policy-mapping
|
||||||
|
policies:
|
||||||
|
- id: docker-official-images
|
||||||
|
description: Docker Official Images
|
||||||
|
files:
|
||||||
|
- path: doi/policy.rego
|
||||||
|
rules:
|
||||||
|
- pattern: "^docker[.]io/library/(.*)$"
|
||||||
|
policy-id: docker-official-images
|
||||||
10
pkg/policy/testdata/mappings/local/mapping.yaml
vendored
Normal file
10
pkg/policy/testdata/mappings/local/mapping.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
version: v1
|
||||||
|
kind: policy-mapping
|
||||||
|
policies:
|
||||||
|
- id: local-policy
|
||||||
|
description: Local Policy
|
||||||
|
files:
|
||||||
|
- path: local-policy.rego
|
||||||
|
rules:
|
||||||
|
- pattern: "^docker[.]io/library/(.*)$"
|
||||||
|
policy-id: docker-official-images # note this policy does not exist in this file
|
||||||
13
pkg/policy/testdata/mappings/rewrite-invalid/mapping.yaml
vendored
Normal file
13
pkg/policy/testdata/mappings/rewrite-invalid/mapping.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
version: v1
|
||||||
|
kind: policy-mapping
|
||||||
|
policies:
|
||||||
|
- id: docker-official-images
|
||||||
|
description: Docker Official Images
|
||||||
|
files:
|
||||||
|
- path: doi/policy.rego
|
||||||
|
rules:
|
||||||
|
- pattern: "^docker[.]io/library/(.*)$"
|
||||||
|
policy-id: docker-official-images
|
||||||
|
- pattern: "^mycoolmirror[.]org/library/(.*)$"
|
||||||
|
rewrite: "docker.io/library/$1"
|
||||||
|
policy-id: docker-official-images # invalid to specify both rewrite and policy-id
|
||||||
14
pkg/policy/testdata/mappings/rewrite-loop/mapping.yaml
vendored
Normal file
14
pkg/policy/testdata/mappings/rewrite-loop/mapping.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
version: v1
|
||||||
|
kind: policy-mapping
|
||||||
|
policies:
|
||||||
|
- id: docker-official-images
|
||||||
|
description: Docker Official Images
|
||||||
|
files:
|
||||||
|
- path: doi/policy.rego
|
||||||
|
rules:
|
||||||
|
- pattern: "^docker[.]io/library/(.*)$"
|
||||||
|
policy-id: docker-official-images
|
||||||
|
- pattern: "^yin/(.*)$"
|
||||||
|
rewrite: "yang/$1"
|
||||||
|
- pattern: "^yang/(.*)$"
|
||||||
|
rewrite: "yin/$1"
|
||||||
14
pkg/policy/testdata/mappings/rewrite-multiple/mapping.yaml
vendored
Normal file
14
pkg/policy/testdata/mappings/rewrite-multiple/mapping.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
version: v1
|
||||||
|
kind: policy-mapping
|
||||||
|
policies:
|
||||||
|
- id: docker-official-images
|
||||||
|
description: Docker Official Images
|
||||||
|
files:
|
||||||
|
- path: doi/policy.rego
|
||||||
|
rules:
|
||||||
|
- pattern: "^docker[.]io/library/(.*)$"
|
||||||
|
policy-id: docker-official-images
|
||||||
|
- pattern: "^mycoolmirror[.]org/library/(.*)$"
|
||||||
|
rewrite: "docker.io/library/$1"
|
||||||
|
- pattern: "^myevencoolermirror[.]org/library/(.*)$"
|
||||||
|
rewrite: "mycoolmirror.org/library/$1"
|
||||||
12
pkg/policy/testdata/mappings/rewrite-to-local/mapping.yaml
vendored
Normal file
12
pkg/policy/testdata/mappings/rewrite-to-local/mapping.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
version: v1
|
||||||
|
kind: policy-mapping
|
||||||
|
policies:
|
||||||
|
- id: local-policy
|
||||||
|
description: Local Policy
|
||||||
|
files:
|
||||||
|
- path: local-policy.rego
|
||||||
|
rules:
|
||||||
|
- pattern: "^docker[.]io/library/(.*)$"
|
||||||
|
policy-id: docker-official-images # note this policy does not exist in this file
|
||||||
|
- pattern: "^mycoolmirror[.]org/library/(.*)$"
|
||||||
|
rewrite: "docker.io/library/$1"
|
||||||
12
pkg/policy/testdata/mappings/rewrite-to-no-match/mapping.yaml
vendored
Normal file
12
pkg/policy/testdata/mappings/rewrite-to-no-match/mapping.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
version: v1
|
||||||
|
kind: policy-mapping
|
||||||
|
policies:
|
||||||
|
- id: docker-official-images
|
||||||
|
description: Docker Official Images
|
||||||
|
files:
|
||||||
|
- path: doi/policy.rego
|
||||||
|
rules:
|
||||||
|
- pattern: "^docker[.]io/library/(.*)$"
|
||||||
|
policy-id: docker-official-images
|
||||||
|
- pattern: "^mycoolmirror[.]org/library/(.*)$"
|
||||||
|
rewrite: "badredirect.org/$1" # no matching rule for this rewrite
|
||||||
12
pkg/policy/testdata/mappings/simple-rewrite/mapping.yaml
vendored
Normal file
12
pkg/policy/testdata/mappings/simple-rewrite/mapping.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
version: v1
|
||||||
|
kind: policy-mapping
|
||||||
|
policies:
|
||||||
|
- id: docker-official-images
|
||||||
|
description: Docker Official Images
|
||||||
|
files:
|
||||||
|
- path: doi/policy.rego
|
||||||
|
rules:
|
||||||
|
- pattern: "^docker[.]io/library/(.*)$"
|
||||||
|
policy-id: docker-official-images
|
||||||
|
- pattern: "^mycoolmirror[.]org/library/(.*)$"
|
||||||
|
rewrite: "docker.io/library/$1"
|
||||||
@@ -2,15 +2,16 @@
|
|||||||
version: v1
|
version: v1
|
||||||
kind: policy-mapping
|
kind: policy-mapping
|
||||||
policies:
|
policies:
|
||||||
- origin:
|
- id: docker-official-images
|
||||||
domain: docker.io
|
|
||||||
prefix: library/
|
|
||||||
id: docker-official-images
|
|
||||||
description: Docker Official Images
|
description: Docker Official Images
|
||||||
|
attestations:
|
||||||
|
repo: "localhost:5001/library-refs"
|
||||||
files:
|
files:
|
||||||
- path: doi/policy.rego
|
- path: doi/policy.rego
|
||||||
mirrors:
|
rules:
|
||||||
- policy-id: docker-official-images
|
- pattern: "^docker[.]io/library/(.*)$"
|
||||||
mirror:
|
policy-id: docker-official-images
|
||||||
domains: [localhost:5001, registry.local:5000]
|
- pattern: ^localhost:5001/(.*)$
|
||||||
prefix: ""
|
rewrite: docker.io/library/$1
|
||||||
|
- pattern: ^registry[.]local:5000/(.*)$
|
||||||
|
rewrite: docker.io/library/$1
|
||||||
|
|||||||
17
pkg/policy/testdata/mock-tuf-allow/mapping.yaml
vendored
17
pkg/policy/testdata/mock-tuf-allow/mapping.yaml
vendored
@@ -2,17 +2,16 @@
|
|||||||
version: v1
|
version: v1
|
||||||
kind: policy-mapping
|
kind: policy-mapping
|
||||||
policies:
|
policies:
|
||||||
- origin:
|
- id: docker-official-images
|
||||||
domain: docker.io
|
|
||||||
prefix: library/
|
|
||||||
id: docker-official-images
|
|
||||||
description: Docker Official Images
|
description: Docker Official Images
|
||||||
attestations:
|
attestations:
|
||||||
repo: "localhost:5001/library-refs"
|
repo: "localhost:5001/library-refs"
|
||||||
files:
|
files:
|
||||||
- path: doi/policy.rego
|
- path: doi/policy.rego
|
||||||
mirrors:
|
rules:
|
||||||
- policy-id: docker-official-images
|
- pattern: "^docker[.]io/library/(.*)$"
|
||||||
mirror:
|
policy-id: docker-official-images
|
||||||
domains: [localhost:5001, registry.local:5000]
|
- pattern: ^localhost:5001/(.*)$
|
||||||
prefix: ""
|
rewrite: docker.io/library/$1
|
||||||
|
- pattern: ^registry[.]local:5000/(.*)$
|
||||||
|
rewrite: docker.io/library/$1
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
version: v1
|
version: v1
|
||||||
kind: policy-mapping
|
kind: policy-mapping
|
||||||
policies:
|
policies:
|
||||||
- origin:
|
- id: docker-official-images
|
||||||
domain: docker.io
|
|
||||||
prefix: library/
|
|
||||||
id: docker-official-images
|
|
||||||
description: Docker Official Images
|
description: Docker Official Images
|
||||||
files:
|
files:
|
||||||
- path: doi/policy.rego
|
- path: doi/policy.rego
|
||||||
|
rules:
|
||||||
|
- pattern: "^docker[.]io/library/(.*)$"
|
||||||
|
policy-id: docker-official-images
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
version: v1
|
version: v1
|
||||||
kind: policy-mapping
|
kind: policy-mapping
|
||||||
policies:
|
policies:
|
||||||
- origin:
|
- id: docker-official-images
|
||||||
domain: docker.io
|
|
||||||
prefix: library/
|
|
||||||
id: docker-official-images
|
|
||||||
description: Docker Official Images
|
description: Docker Official Images
|
||||||
files:
|
files:
|
||||||
- path: doi/policy.rego
|
- path: doi/policy.rego
|
||||||
|
rules:
|
||||||
|
- pattern: "^docker[.]io/library/(.*)$"
|
||||||
|
policy-id: docker-official-images
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
version: v1
|
version: v1
|
||||||
kind: policy-mapping
|
kind: policy-mapping
|
||||||
policies:
|
policies:
|
||||||
- origin:
|
- id: docker-official-images
|
||||||
domain: docker.io
|
|
||||||
prefix: library/
|
|
||||||
id: docker-official-images
|
|
||||||
description: Docker Official Images
|
description: Docker Official Images
|
||||||
files:
|
files:
|
||||||
- path: doi/policy.rego
|
- path: doi/policy.rego
|
||||||
|
rules:
|
||||||
|
- pattern: "^docker[.]io/library/(.*)$"
|
||||||
|
policy-id: docker-official-images
|
||||||
|
|||||||
@@ -36,9 +36,10 @@ type PolicyOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Policy struct {
|
type Policy struct {
|
||||||
InputFiles []*PolicyFile
|
InputFiles []*PolicyFile
|
||||||
Query string
|
Query string
|
||||||
Mapping *config.PolicyMapping
|
Mapping *config.PolicyMapping
|
||||||
|
ResolvedName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type PolicyInput struct {
|
type PolicyInput struct {
|
||||||
|
|||||||
11
test/testdata/local-policy-fail/mapping.yaml
vendored
11
test/testdata/local-policy-fail/mapping.yaml
vendored
@@ -1,11 +1,10 @@
|
|||||||
# map repos to policies
|
|
||||||
version: v1
|
version: v1
|
||||||
kind: policy-mapping
|
kind: policy-mapping
|
||||||
policies:
|
policies:
|
||||||
- origin:
|
- id: test-images
|
||||||
domain: docker.io
|
|
||||||
prefix: library/
|
|
||||||
id: test-images
|
|
||||||
description: Local test images
|
description: Local test images
|
||||||
files:
|
files:
|
||||||
- path: doi/policy.rego
|
- path: policy.rego
|
||||||
|
rules:
|
||||||
|
- pattern: ".*"
|
||||||
|
policy-id: test-images
|
||||||
|
|||||||
12
test/testdata/local-policy-mirror/mapping.yaml
vendored
Normal file
12
test/testdata/local-policy-mirror/mapping.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
version: v1
|
||||||
|
kind: policy-mapping
|
||||||
|
policies:
|
||||||
|
- id: test-images
|
||||||
|
description: Local test images
|
||||||
|
files:
|
||||||
|
- path: policy.rego
|
||||||
|
rules:
|
||||||
|
- pattern: "^docker[.]io/library/test-image$"
|
||||||
|
policy-id: test-images
|
||||||
|
- pattern: "^mirror[.]org/library/(.*)$"
|
||||||
|
rewrite: docker.io/library/$1
|
||||||
54
test/testdata/local-policy-mirror/policy.rego
vendored
Normal file
54
test/testdata/local-policy-mirror/policy.rego
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package attest
|
||||||
|
|
||||||
|
import rego.v1
|
||||||
|
|
||||||
|
keys := [{
|
||||||
|
"id": "6b241993defaba26558c64f94a94303ce860e7ad9163d801495c91cf57197c75",
|
||||||
|
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZmicqYSY38DprGr42jU0V3ND0ROj\nzSRH1+yjsxhh0bi52Hh/DuOhrSq2KJ5a09lW3ybnDjljowbkof0Y1i9Oow==\n-----END PUBLIC KEY-----",
|
||||||
|
"from": "2023-12-15T14:00:00Z",
|
||||||
|
"to": null,
|
||||||
|
# this key is still active
|
||||||
|
"status": "active",
|
||||||
|
"signing-format": "dssev1",
|
||||||
|
}]
|
||||||
|
|
||||||
|
provs(pred) := p if {
|
||||||
|
res := attest.fetch(pred)
|
||||||
|
not res.error
|
||||||
|
p := res.value
|
||||||
|
}
|
||||||
|
|
||||||
|
atts := union({
|
||||||
|
provs("https://slsa.dev/provenance/v0.2"),
|
||||||
|
provs("https://spdx.dev/Document"),
|
||||||
|
})
|
||||||
|
|
||||||
|
opts := {"keys": keys}
|
||||||
|
|
||||||
|
statements contains s if {
|
||||||
|
some att in atts
|
||||||
|
res := attest.verify(att, opts)
|
||||||
|
not res.error
|
||||||
|
s := res.value
|
||||||
|
}
|
||||||
|
|
||||||
|
subjects contains subject if {
|
||||||
|
some statement in statements
|
||||||
|
some subject in statement.subject
|
||||||
|
}
|
||||||
|
|
||||||
|
success if {
|
||||||
|
print("input:",input)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
result := {
|
||||||
|
"success": success,
|
||||||
|
"violations": set(),
|
||||||
|
"summary": {
|
||||||
|
"subjects": subjects,
|
||||||
|
"slsa_levels": ["SLSA_BUILD_LEVEL_3"],
|
||||||
|
"verifier": "docker-official-images",
|
||||||
|
"policy_uri": "https://docker.com/official/policy/v0.1",
|
||||||
|
},
|
||||||
|
}
|
||||||
11
test/testdata/local-policy-no-tl/mapping.yaml
vendored
11
test/testdata/local-policy-no-tl/mapping.yaml
vendored
@@ -1,11 +1,10 @@
|
|||||||
# map repos to policies
|
|
||||||
version: v1
|
version: v1
|
||||||
kind: policy-mapping
|
kind: policy-mapping
|
||||||
policies:
|
policies:
|
||||||
- origin:
|
- id: test-images
|
||||||
domain: docker.io
|
|
||||||
prefix: library/
|
|
||||||
id: test-images
|
|
||||||
description: Local test images
|
description: Local test images
|
||||||
files:
|
files:
|
||||||
- path: doi/policy.rego
|
- path: policy.rego
|
||||||
|
rules:
|
||||||
|
- pattern: ".*"
|
||||||
|
policy-id: test-images
|
||||||
|
|||||||
16
test/testdata/local-policy-pass/mapping.yaml
vendored
16
test/testdata/local-policy-pass/mapping.yaml
vendored
@@ -1,16 +1,10 @@
|
|||||||
# map repos to policies
|
|
||||||
version: v1
|
version: v1
|
||||||
kind: policy-mapping
|
kind: policy-mapping
|
||||||
policies:
|
policies:
|
||||||
- origin:
|
- id: test-images
|
||||||
domain: docker.io
|
|
||||||
prefix: library/
|
|
||||||
id: test-images
|
|
||||||
description: Local test images
|
description: Local test images
|
||||||
files:
|
files:
|
||||||
- path: doi/policy.rego
|
- path: policy.rego
|
||||||
mirrors:
|
rules:
|
||||||
- policy-id: test-images
|
- pattern: ".*"
|
||||||
mirror:
|
policy-id: test-images
|
||||||
domains: ["*"]
|
|
||||||
prefix: ""
|
|
||||||
|
|||||||
39
test/testdata/local-policy/mapping.yaml
vendored
39
test/testdata/local-policy/mapping.yaml
vendored
@@ -1,29 +1,18 @@
|
|||||||
# map repos to policies
|
|
||||||
version: v1
|
version: v1
|
||||||
kind: policy-mapping
|
kind: policy-mapping
|
||||||
policies:
|
policies:
|
||||||
- origin:
|
- id: docker-official-images
|
||||||
domain: docker.io
|
description: Docker Official Images
|
||||||
prefix: library/
|
|
||||||
id: test-images
|
|
||||||
description: Local test images
|
|
||||||
files:
|
files:
|
||||||
- path: "doi/policy.rego"
|
- path: doi/policy.rego
|
||||||
|
rules:
|
||||||
mirrors:
|
- pattern: "^docker[.]io/library/(.*)$"
|
||||||
- policy-id: test-images
|
policy-id: docker-official-images
|
||||||
mirror:
|
- pattern: "repo$"
|
||||||
domains: ["*"]
|
policy-id: docker-official-images
|
||||||
prefix: "repo"
|
- pattern: "test-image$"
|
||||||
- policy-id: test-images
|
policy-id: docker-official-images
|
||||||
mirror:
|
- pattern: "image-signer-verifier-test$"
|
||||||
domains: ["*"]
|
policy-id: docker-official-images
|
||||||
prefix: "library/"
|
- pattern: "library/(.*)$"
|
||||||
- policy-id: test-images
|
rewrite: docker.io/library/$1
|
||||||
mirror:
|
|
||||||
domains: ["*"]
|
|
||||||
prefix: "test-image"
|
|
||||||
- policy-id: test-images
|
|
||||||
mirror:
|
|
||||||
domains: ["*"]
|
|
||||||
prefix: "image-signer-verifier-test"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user