From 802725caf0fcf6232ac3efaa2818dc5ab44c194a Mon Sep 17 00:00:00 2001 From: James Carnegie Date: Wed, 21 Aug 2024 18:01:11 +0100 Subject: [PATCH] feat: add purl details to policy inputs (#129) --- README.md | 5 ++- pkg/attest/sign_test.go | 1 + pkg/attest/verify.go | 28 ++++++++++-- pkg/attest/verify_test.go | 9 +++- pkg/oci/oci.go | 6 +-- pkg/oci/oci_test.go | 40 ++++++++++------- pkg/policy/policy_test.go | 24 ++++++----- .../mock-tuf-allow-canonical/doi/policy.rego | 8 +++- pkg/policy/types.go | 10 +++-- .../local-policy-inputs/doi/policy.rego | 43 +++++++++++++++++++ .../testdata/local-policy-inputs/mapping.yaml | 18 ++++++++ test/testdata/local-policy-mirror/policy.rego | 2 +- 12 files changed, 150 insertions(+), 44 deletions(-) create mode 100644 test/testdata/local-policy-inputs/doi/policy.rego create mode 100644 test/testdata/local-policy-inputs/mapping.yaml diff --git a/README.md b/README.md index 0656118..a4fabab 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,10 @@ The input to the policy is an object with the following fields: - `digest` (string): the digest of the image being verified - `purl` (string): the package URL of the image being verified -- `is_canonical` (bool): whether the image being verified was referenced by a 'canonical' name, i.e. one that contains a digest +- `platform` (string): the platform of the image being verified +- `normalized_name` (string): defaults are filled out. e.g. if the image is `alpine`, this would be `library/alpine` +- `familiar_name` (string): short version of above (e.g. `alpine`) +- `tag`: (string): tag of the image being verified (if present) ### Builtin Functions diff --git a/pkg/attest/sign_test.go b/pkg/attest/sign_test.go index 3d6a602..27b821a 100644 --- a/pkg/attest/sign_test.go +++ b/pkg/attest/sign_test.go @@ -20,6 +20,7 @@ var ( PassMirrorPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-mirror") PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl") FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail") + InputsPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-inputs") TestTempDir = "attest-sign-test" ) diff --git a/pkg/attest/verify.go b/pkg/attest/verify.go index d30082e..16571b2 100644 --- a/pkg/attest/verify.go +++ b/pkg/attest/verify.go @@ -140,14 +140,34 @@ func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, pctx name = strings.Replace(name, oldName, pctx.ResolvedName, 1) } - purl, canonical, err := oci.RefToPURL(name, platform) + ref, err := reference.ParseNormalizedNamed(name) + if err != nil { + return nil, fmt.Errorf("failed to parse ref %q: %w", ref, err) + } + purl, canonical, err := oci.RefToPURL(ref, platform) if err != nil { return nil, fmt.Errorf("failed to convert ref to purl: %w", err) } + var tag string + if !canonical { + // unlike the function name indicates, this adds latest if no tag is present + ref = reference.TagNameOnly(ref) + } + + if tagged, ok := ref.(reference.Tagged); ok { + tag = tagged.Tag() + } input := &policy.Input{ - Digest: digest, - PURL: purl, - IsCanonical: canonical, + Digest: digest, + PURL: purl, + Platform: platform.String(), + Domain: reference.Domain(ref), + NormalizedName: reference.Path(ref), + FamiliarName: reference.FamiliarName(ref), + } + // rego has null strings + if tag != "" { + input.Tag = tag } evaluator, err := policy.GetPolicyEvaluator(ctx) diff --git a/pkg/attest/verify_test.go b/pkg/attest/verify_test.go index b386619..70e919f 100644 --- a/pkg/attest/verify_test.go +++ b/pkg/attest/verify_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "testing" + "github.com/distribution/reference" "github.com/docker/attest/internal/test" "github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/config" @@ -98,7 +99,7 @@ func TestVSA(t *testing.T) { if assert.NotNil(t, results.Input) { assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", results.Input.Digest) - assert.False(t, results.Input.IsCanonical) + assert.NotNil(t, results.Input.Tag) } assert.Equal(t, intoto.StatementInTotoV01, results.VSA.Type) @@ -187,6 +188,7 @@ func TestSignVerify(t *testing.T) { {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}, + {name: "verify inputs", signTL: false, policyDir: InputsPolicyDir}, } attIdx, err := oci.IndexFromPath(test.UnsignedTestImage) @@ -226,7 +228,10 @@ func TestSignVerify(t *testing.T) { assert.Equal(t, OutcomeSuccess, results.Outcome) platform, err := oci.ParsePlatform(LinuxAMD64) require.NoError(t, err) - expectedPURL, _, err := oci.RefToPURL(attIdx.Name, platform) + + ref, err := reference.ParseNormalizedNamed(attIdx.Name) + require.NoError(t, err) + expectedPURL, _, err := oci.RefToPURL(ref, platform) require.NoError(t, err) assert.Equal(t, expectedPURL, results.Input.PURL) }) diff --git a/pkg/oci/oci.go b/pkg/oci/oci.go index f5cc573..3d52ecd 100644 --- a/pkg/oci/oci.go +++ b/pkg/oci/oci.go @@ -52,12 +52,8 @@ func ImageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descripto return nil, fmt.Errorf("no image found for platform %v", platform) } -func RefToPURL(ref string, platform *v1.Platform) (string, bool, error) { +func RefToPURL(named reference.Named, platform *v1.Platform) (string, bool, error) { var isCanonical bool - named, err := reference.ParseNormalizedNamed(ref) - if err != nil { - return "", false, fmt.Errorf("failed to parse ref %q: %w", ref, err) - } var qualifiers []packageurl.Qualifier if canonical, ok := named.(reference.Canonical); ok { diff --git a/pkg/oci/oci_test.go b/pkg/oci/oci_test.go index 03937d0..898f5af 100644 --- a/pkg/oci/oci_test.go +++ b/pkg/oci/oci_test.go @@ -3,6 +3,7 @@ package oci_test import ( "testing" + "github.com/distribution/reference" "github.com/docker/attest/internal/test" "github.com/docker/attest/pkg/oci" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -14,42 +15,51 @@ import ( func TestRefToPurl(t *testing.T) { arm, err := oci.ParsePlatform("arm64/linux") require.NoError(t, err) - purl, canonical, err := oci.RefToPURL("alpine", arm) + ref, err := reference.ParseNormalizedNamed("alpine") + require.NoError(t, err) + purl, canonical, err := oci.RefToPURL(ref, arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/alpine@latest?platform=arm64%2Flinux", purl) assert.False(t, canonical) - - purl, canonical, err = oci.RefToPURL("alpine:123", arm) + ref, err = reference.ParseNormalizedNamed("alpine:123") + require.NoError(t, err) + purl, canonical, err = oci.RefToPURL(ref, arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - - purl, canonical, err = oci.RefToPURL("google/alpine:123", arm) + ref, err = reference.ParseNormalizedNamed("google/alpine:123") + require.NoError(t, err) + purl, canonical, err = oci.RefToPURL(ref, arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/google/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - - purl, canonical, err = oci.RefToPURL("library/alpine:123", arm) + ref, err = reference.ParseNormalizedNamed("library/alpine:123") + require.NoError(t, err) + purl, canonical, err = oci.RefToPURL(ref, arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - - purl, canonical, err = oci.RefToPURL("docker.io/library/alpine:123", arm) + ref, err = reference.ParseNormalizedNamed("docker.io/library/alpine:123") + require.NoError(t, err) + purl, canonical, err = oci.RefToPURL(ref, arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - - purl, canonical, err = oci.RefToPURL("localhost:5001/library/alpine:123", arm) + ref, err = reference.ParseNormalizedNamed("localhost:5001/library/alpine:123") + require.NoError(t, err) + purl, canonical, err = oci.RefToPURL(ref, arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/localhost%3A5001/library/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - - purl, canonical, err = oci.RefToPURL("localhost:5001/alpine:123", arm) + ref, err = reference.ParseNormalizedNamed("localhost:5001/alpine:123") + require.NoError(t, err) + purl, canonical, err = oci.RefToPURL(ref, arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/localhost%3A5001/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - - purl, canonical, err = oci.RefToPURL("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b", arm) + ref, err = reference.ParseNormalizedNamed("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b") + require.NoError(t, err) + purl, canonical, err = oci.RefToPURL(ref, arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/localhost%3A5001/alpine?digest=sha256%3Ac5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b&platform=arm64%2Flinux", purl) assert.True(t, canonical) diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index b282daf..ac50122 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -51,23 +51,25 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { policyID string resolveErrorStr string }{ - {repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver}, - {repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver, policyID: "docker-official-images"}, - {repo: "testdata/mock-tuf-allow", expectSuccess: false, isCanonical: false, resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr}, - {repo: "testdata/mock-tuf-deny", expectSuccess: false, isCanonical: false, resolver: defaultResolver}, - {repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, isCanonical: false, resolver: defaultResolver}, - {repo: "testdata/mock-tuf-wrong-key", expectSuccess: false, isCanonical: false, resolver: defaultResolver}, + {repo: "testdata/mock-tuf-allow", expectSuccess: true, resolver: defaultResolver}, + {repo: "testdata/mock-tuf-allow", expectSuccess: true, resolver: defaultResolver, policyID: "docker-official-images"}, + {repo: "testdata/mock-tuf-allow", resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr}, + {repo: "testdata/mock-tuf-deny", resolver: defaultResolver}, + {repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, resolver: defaultResolver}, + {repo: "testdata/mock-tuf-wrong-key", resolver: defaultResolver}, {repo: "testdata/mock-tuf-allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver}, - {repo: "testdata/mock-tuf-allow-canonical", expectSuccess: false, isCanonical: false, resolver: defaultResolver}, - {repo: "testdata/mock-tuf-no-rego", expectSuccess: false, isCanonical: false, resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"}, + {repo: "testdata/mock-tuf-allow-canonical", resolver: defaultResolver}, + {repo: "testdata/mock-tuf-no-rego", resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"}, } for _, tc := range testCases { t.Run(tc.repo, func(t *testing.T) { input := &policy.Input{ - Digest: "sha256:test-digest", - PURL: "test-purl", - IsCanonical: tc.isCanonical, + Digest: "sha256:test-digest", + PURL: "test-purl", + } + if !tc.isCanonical { + input.Tag = "test" } tufClient := tuf.NewMockTufClient(tc.repo, test.CreateTempDir(t, "", "tuf-dest")) diff --git a/pkg/policy/testdata/mock-tuf-allow-canonical/doi/policy.rego b/pkg/policy/testdata/mock-tuf-allow-canonical/doi/policy.rego index aa197a2..295228b 100644 --- a/pkg/policy/testdata/mock-tuf-allow-canonical/doi/policy.rego +++ b/pkg/policy/testdata/mock-tuf-allow-canonical/doi/policy.rego @@ -2,6 +2,10 @@ package attest import rego.v1 -result := { - "success": input.isCanonical, +default canonical = false + +canonical if { + not input.tag } + +result := {"success": canonical} diff --git a/pkg/policy/types.go b/pkg/policy/types.go index a122264..e62cf0c 100644 --- a/pkg/policy/types.go +++ b/pkg/policy/types.go @@ -45,9 +45,13 @@ type Policy struct { } type Input struct { - Digest string `json:"digest"` - PURL string `json:"purl"` - IsCanonical bool `json:"isCanonical"` + Digest string `json:"digest"` + PURL string `json:"purl"` + Tag string `json:"tag,omitempty"` + Domain string `json:"domain"` + NormalizedName string `json:"normalized_name"` + FamiliarName string `json:"familiar_name"` + Platform string `json:"platform"` } type File struct { diff --git a/test/testdata/local-policy-inputs/doi/policy.rego b/test/testdata/local-policy-inputs/doi/policy.rego new file mode 100644 index 0000000..0507909 --- /dev/null +++ b/test/testdata/local-policy-inputs/doi/policy.rego @@ -0,0 +1,43 @@ +package attest + +import rego.v1 + +keys := [{ + "id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4", + "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN\n9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw==\n-----END PUBLIC KEY-----", + "from": "2023-12-15T14:00:00Z", + "to": null, + "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"), +}) + +success if { + input.domain == "docker.io" + input.familiar_name == "test-image" + input.normalized_name == "library/test-image" + input.platform == "linux/amd64" + input.tag == "test" +} + +result := { + "success": success, + "violations": set(), + "attestations": set(), + "summary": { + "subjects": set(), + "slsa_level": "SLSA_BUILD_LEVEL_3", + "verifier": "docker-official-images", + "policy_uri": "https://docker.com/official/policy/v0.1", + }, +} diff --git a/test/testdata/local-policy-inputs/mapping.yaml b/test/testdata/local-policy-inputs/mapping.yaml new file mode 100644 index 0000000..1c3bdbe --- /dev/null +++ b/test/testdata/local-policy-inputs/mapping.yaml @@ -0,0 +1,18 @@ +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: "repo$" + policy-id: docker-official-images + - pattern: "test-image$" + policy-id: docker-official-images + - pattern: "image-signer-verifier-test$" + policy-id: docker-official-images + - pattern: "library/(.*)$" + rewrite: docker.io/library/$1 diff --git a/test/testdata/local-policy-mirror/policy.rego b/test/testdata/local-policy-mirror/policy.rego index 2922834..6e36fc8 100644 --- a/test/testdata/local-policy-mirror/policy.rego +++ b/test/testdata/local-policy-mirror/policy.rego @@ -38,7 +38,7 @@ subjects contains subject if { } success if { - print("input:",input) + # print("input:",input) true }