From 5db1b5c4c1408c7c6fd3bb2bad682750c3637e88 Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Wed, 28 Aug 2024 16:13:06 -0500 Subject: [PATCH 1/2] feat: add tuf resolver test --- pkg/policy/policy_test.go | 24 +++---- pkg/policy/resolver_test.go | 65 +++++++++++++++++++ .../testdata/policies/no-policy/mapping.yaml | 10 +++ pkg/tuf/mock.go | 62 ++++++++++++++++++ 4 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 pkg/policy/resolver_test.go create mode 100644 pkg/policy/testdata/policies/no-policy/mapping.yaml diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index 239e637..a6529b6 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -42,7 +42,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { } testCases := []struct { - repo string + policyPath string expectSuccess bool isCanonical bool resolver attestation.Resolver @@ -50,19 +50,19 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { policyID string resolveErrorStr string }{ - {repo: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver}, - {repo: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver, policyID: "docker-official-images"}, - {repo: "testdata/policies/allow", resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr}, - {repo: "testdata/policies/deny", resolver: defaultResolver}, - {repo: "testdata/policies/verify-sig", expectSuccess: true, resolver: defaultResolver}, - {repo: "testdata/policies/wrong-key", resolver: defaultResolver}, - {repo: "testdata/policies/allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver}, - {repo: "testdata/policies/allow-canonical", resolver: defaultResolver}, - {repo: "testdata/policies/no-rego", resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"}, + {policyPath: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver}, + {policyPath: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver, policyID: "docker-official-images"}, + {policyPath: "testdata/policies/allow", resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr}, + {policyPath: "testdata/policies/deny", resolver: defaultResolver}, + {policyPath: "testdata/policies/verify-sig", expectSuccess: true, resolver: defaultResolver}, + {policyPath: "testdata/policies/wrong-key", resolver: defaultResolver}, + {policyPath: "testdata/policies/allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver}, + {policyPath: "testdata/policies/allow-canonical", resolver: defaultResolver}, + {policyPath: "testdata/policies/no-rego", resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"}, } for _, tc := range testCases { - t.Run(tc.repo, func(t *testing.T) { + t.Run(tc.policyPath, func(t *testing.T) { input := &policy.Input{ Digest: "sha256:test-digest", PURL: "test-purl", @@ -75,7 +75,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { tc.opts = &policy.Options{ LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"), PolicyID: tc.policyID, - LocalPolicyDir: tc.repo, + LocalPolicyDir: tc.policyPath, DisableTUF: true, } } diff --git a/pkg/policy/resolver_test.go b/pkg/policy/resolver_test.go new file mode 100644 index 0000000..bfde24c --- /dev/null +++ b/pkg/policy/resolver_test.go @@ -0,0 +1,65 @@ +package policy_test + +import ( + "context" + "testing" + + "github.com/docker/attest/internal/test" + "github.com/docker/attest/pkg/policy" + "github.com/docker/attest/pkg/tuf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResolvePolicy(t *testing.T) { + localPolicyPath := "testdata/policies/allow" + tufPolicyPath := "testdata/policies/allow-canonical" + noLocalPolicyPath := "testdata/policies/no-policy" + testPolicyID := "docker-official-images" + testImageName := "localhost:5001/test/repo:tag" + + testCases := []struct { + name string + policyPath string + policyID string + localOverridesTUF bool // if a policy is provided locally, it should override TUF + DisableTUF bool + }{ + {name: "resolve by id (TUF only)", policyID: testPolicyID, DisableTUF: false}, + {name: "resolve by id (local mapping, TUF policy)", policyPath: noLocalPolicyPath, policyID: testPolicyID, DisableTUF: false}, + {name: "resolve by id (local mapping, local policy, no TUF)", policyPath: localPolicyPath, policyID: testPolicyID, DisableTUF: true}, + {name: "resolve by id (local mapping, local policy)", policyPath: localPolicyPath, policyID: testPolicyID, DisableTUF: false, localOverridesTUF: true}, + {name: "resolve by match (TUF only)", DisableTUF: false}, + {name: "resolve by match (local mapping, TUF policy)", policyPath: noLocalPolicyPath, DisableTUF: false}, + {name: "resolve by match (local mapping, local policy, no TUF)", policyPath: localPolicyPath, DisableTUF: true}, + {name: "resolve by match (local mapping, local policy)", policyPath: localPolicyPath, DisableTUF: false, localOverridesTUF: true}, + } + + var tufClient tuf.Downloader + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + opts := &policy.Options{} + tempDir := test.CreateTempDir(t, "", "tuf-dest") + if !tc.DisableTUF { + tufClient = tuf.NewMockTufClient(tufPolicyPath, tempDir) + } + if tc.policyID != "" { + opts.PolicyID = tc.policyID + } + if tc.policyPath != "" { + opts.LocalPolicyDir = tc.policyPath + } + opts.DisableTUF = tc.DisableTUF + opts.LocalTargetsDir = tempDir + resolver := policy.NewResolver(tufClient, opts) + policy, err := resolver.ResolvePolicy(context.Background(), testImageName) + require.NoError(t, err) + assert.NotNil(t, policy) + if tc.DisableTUF || tc.localOverridesTUF { + assert.Contains(t, policy.URI, localPolicyPath) + } else { + assert.Contains(t, policy.URI, tufPolicyPath) + } + }) + } +} diff --git a/pkg/policy/testdata/policies/no-policy/mapping.yaml b/pkg/policy/testdata/policies/no-policy/mapping.yaml new file mode 100644 index 0000000..6da334a --- /dev/null +++ b/pkg/policy/testdata/policies/no-policy/mapping.yaml @@ -0,0 +1,10 @@ +# map repos to policies +version: v1 +kind: policy-mapping +rules: + - pattern: "^docker[.]io/library/(.*)$" + policy-id: docker-official-images + - pattern: ^localhost:5001/(.*)$ + rewrite: docker.io/library/$1 + - pattern: ^registry[.]local:5000/(.*)$ + rewrite: docker.io/library/$1 diff --git a/pkg/tuf/mock.go b/pkg/tuf/mock.go index ca742a8..9e0f870 100644 --- a/pkg/tuf/mock.go +++ b/pkg/tuf/mock.go @@ -1,5 +1,67 @@ package tuf +import ( + "io" + "os" + "path/filepath" + + "github.com/docker/attest/internal/util" +) + +type MockTufClient struct { + srcPath string + dstPath string +} + +func NewMockTufClient(srcPath string, dstPath string) *MockTufClient { + if srcPath == "" { + panic("srcPath must be set") + } + if dstPath == "" { + panic("dstPath must be set") + } + return &MockTufClient{ + srcPath: srcPath, + dstPath: dstPath, + } +} + +func (dc *MockTufClient) DownloadTarget(target string, filePath string) (file *TargetFile, err error) { + targetPath := filepath.Join(dc.srcPath, target) + src, err := os.Open(targetPath) + if err != nil { + return nil, err + } + defer src.Close() + + var dstFilePath string + if filePath == "" { + dstFilePath = filepath.Join(dc.dstPath, filepath.FromSlash(target)) + } else { + dstFilePath = filePath + } + + err = os.MkdirAll(filepath.Dir(dstFilePath), os.ModePerm) + if err != nil { + return nil, err + } + dst, err := os.Create(dstFilePath) + if err != nil { + return nil, err + } + defer dst.Close() + + // reading from tee will read from src and write to dst at the same time + tee := io.TeeReader(src, dst) + + b, err := io.ReadAll(tee) + if err != nil { + return nil, err + } + + return &TargetFile{ActualFilePath: dstFilePath, TargetURI: targetPath, Data: b, Digest: util.SHA256Hex(b)}, nil +} + type MockVersionChecker struct { err error } From 2acc30693f133ab33676bc0dc125e4c48271a5c2 Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Thu, 29 Aug 2024 08:25:42 -0500 Subject: [PATCH 2/2] fix: remove mock tuf client output --- pkg/policy/resolver_test.go | 2 +- pkg/tuf/mock.go | 33 ++++----------------------------- 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/pkg/policy/resolver_test.go b/pkg/policy/resolver_test.go index bfde24c..026bd39 100644 --- a/pkg/policy/resolver_test.go +++ b/pkg/policy/resolver_test.go @@ -41,7 +41,7 @@ func TestResolvePolicy(t *testing.T) { opts := &policy.Options{} tempDir := test.CreateTempDir(t, "", "tuf-dest") if !tc.DisableTUF { - tufClient = tuf.NewMockTufClient(tufPolicyPath, tempDir) + tufClient = tuf.NewMockTufClient(tufPolicyPath) } if tc.policyID != "" { opts.PolicyID = tc.policyID diff --git a/pkg/tuf/mock.go b/pkg/tuf/mock.go index 9e0f870..f024535 100644 --- a/pkg/tuf/mock.go +++ b/pkg/tuf/mock.go @@ -10,23 +10,18 @@ import ( type MockTufClient struct { srcPath string - dstPath string } -func NewMockTufClient(srcPath string, dstPath string) *MockTufClient { +func NewMockTufClient(srcPath string) *MockTufClient { if srcPath == "" { panic("srcPath must be set") } - if dstPath == "" { - panic("dstPath must be set") - } return &MockTufClient{ srcPath: srcPath, - dstPath: dstPath, } } -func (dc *MockTufClient) DownloadTarget(target string, filePath string) (file *TargetFile, err error) { +func (dc *MockTufClient) DownloadTarget(target string, _ string) (file *TargetFile, err error) { targetPath := filepath.Join(dc.srcPath, target) src, err := os.Open(targetPath) if err != nil { @@ -34,32 +29,12 @@ func (dc *MockTufClient) DownloadTarget(target string, filePath string) (file *T } defer src.Close() - var dstFilePath string - if filePath == "" { - dstFilePath = filepath.Join(dc.dstPath, filepath.FromSlash(target)) - } else { - dstFilePath = filePath - } - - err = os.MkdirAll(filepath.Dir(dstFilePath), os.ModePerm) - if err != nil { - return nil, err - } - dst, err := os.Create(dstFilePath) - if err != nil { - return nil, err - } - defer dst.Close() - - // reading from tee will read from src and write to dst at the same time - tee := io.TeeReader(src, dst) - - b, err := io.ReadAll(tee) + b, err := io.ReadAll(src) if err != nil { return nil, err } - return &TargetFile{ActualFilePath: dstFilePath, TargetURI: targetPath, Data: b, Digest: util.SHA256Hex(b)}, nil + return &TargetFile{TargetURI: targetPath, Data: b, Digest: util.SHA256Hex(b)}, nil } type MockVersionChecker struct {