From da667de6101cc6b85679cbf3cc2cd3e660a33837 Mon Sep 17 00:00:00 2001 From: James Carnegie Date: Tue, 15 Oct 2024 16:07:26 +0100 Subject: [PATCH] feat: support arbitrary rego input parameters (#196) * feat: support arbitrary rego input parameters --- policy/types.go | 19 +++--- test/testdata/local-policy-param/.gitignore | 1 + test/testdata/local-policy-param/mapping.yaml | 15 +++++ test/testdata/local-policy-param/policy.rego | 61 +++++++++++++++++++ verify.go | 5 +- verify_test.go | 30 +++++++-- 6 files changed, 116 insertions(+), 15 deletions(-) create mode 100644 test/testdata/local-policy-param/.gitignore create mode 100644 test/testdata/local-policy-param/mapping.yaml create mode 100644 test/testdata/local-policy-param/policy.rego diff --git a/policy/types.go b/policy/types.go index 59a0bfb..a14e56a 100644 --- a/policy/types.go +++ b/policy/types.go @@ -38,8 +38,12 @@ type Options struct { AttestationStyle mapping.AttestationStyle Debug bool AttestationVerifier attestation.Verifier + // extra parameters to pass through to rego as policy inputs + Parameters Parameters } +type Parameters map[string]string + type Policy struct { InputFiles []*File Query string @@ -50,13 +54,14 @@ type Policy struct { } type Input struct { - 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"` + 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"` + Parameters Parameters `json:"parameters"` } type File struct { diff --git a/test/testdata/local-policy-param/.gitignore b/test/testdata/local-policy-param/.gitignore new file mode 100644 index 0000000..5b6b072 --- /dev/null +++ b/test/testdata/local-policy-param/.gitignore @@ -0,0 +1 @@ +config.yaml diff --git a/test/testdata/local-policy-param/mapping.yaml b/test/testdata/local-policy-param/mapping.yaml new file mode 100644 index 0000000..2dacf8a --- /dev/null +++ b/test/testdata/local-policy-param/mapping.yaml @@ -0,0 +1,15 @@ +version: v1 +kind: policy-mapping +policies: + - id: test-images + description: Local test images + files: + - path: policy.rego + - path: config.yaml #auto generated + attestations: + style: attached +rules: + - pattern: "^docker[.]io/library/test-image$" + policy-id: test-images + - pattern: "^mirror[.]org/library/(.*)$" + rewrite: docker.io/library/$1 diff --git a/test/testdata/local-policy-param/policy.rego b/test/testdata/local-policy-param/policy.rego new file mode 100644 index 0000000..ed01408 --- /dev/null +++ b/test/testdata/local-policy-param/policy.rego @@ -0,0 +1,61 @@ +package attest + +import rego.v1 + +import data.keys +import input.parameters + +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, "skip_tl": true} + +statements contains s if { + parameters.foo == "bar" + 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 +} + +unsafe_statement_from_attestation(att) := statement if { + payload := att.payload + statement := json.unmarshal(base64.decode(payload)) +} + +violations contains violation if { + some att in atts + statement := unsafe_statement_from_attestation(att) + res := attest.verify(att, opts) + err := res.error + violation := { + "type": "unsigned_statement", + "description": sprintf("Statement is not correctly signed: %v", [err]), + "attestation": statement, + "details": {"error": err}, + } +} + +result := { + "success": count(statements) > 0, + "violations": violations, + "summary": { + "subjects": subjects, + "slsa_level": "SLSA_BUILD_LEVEL_3", + "verifier": "docker-official-images", + "policy_uri": "https://docker.com/official/policy/v0.1", + }, +} diff --git a/verify.go b/verify.go index 04ad31d..4611fd1 100644 --- a/verify.go +++ b/verify.go @@ -93,7 +93,7 @@ func (verifier *ImageVerifier) Verify(ctx context.Context, src *oci.ImageSpec) ( return nil, fmt.Errorf("failed to create attestation resolver: %w", err) } evaluator := policy.NewRegoEvaluator(verifier.opts.Debug, verifier.attestationVerifier) - result, err = VerifyAttestations(ctx, resolver, evaluator, resolvedPolicy) + result, err = verifyAttestations(ctx, resolver, evaluator, resolvedPolicy, verifier.opts) if err != nil { return nil, fmt.Errorf("failed to evaluate policy: %w", err) } @@ -195,7 +195,7 @@ func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy. }, nil } -func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, evaluator policy.Evaluator, resolvedPolicy *policy.Policy) (*VerificationResult, error) { +func verifyAttestations(ctx context.Context, resolver attestation.Resolver, evaluator policy.Evaluator, resolvedPolicy *policy.Policy, opts *policy.Options) (*VerificationResult, error) { desc, err := resolver.ImageDescriptor(ctx) if err != nil { return nil, fmt.Errorf("failed to get image descriptor: %w", err) @@ -247,6 +247,7 @@ func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, eval Domain: reference.Domain(ref), NormalizedName: reference.Path(ref), FamiliarName: reference.FamiliarName(ref), + Parameters: opts.Parameters, } // rego has null strings if tag != "" { diff --git a/verify_test.go b/verify_test.go index dac83ef..09546f8 100644 --- a/verify_test.go +++ b/verify_test.go @@ -27,6 +27,8 @@ import ( var ( ExampleAttestation = filepath.Join("test", "testdata", "example_attestation.json") LocalKeysPolicy = filepath.Join("test", "testdata", "local-policy-real") + LocalParamPolicy = filepath.Join("test", "testdata", "local-policy-param") + ExpiresPolicy = filepath.Join("test", "testdata", "expires") ) const ( @@ -60,7 +62,7 @@ func TestVerifyAttestations(t *testing.T) { return policy.AllowedResult(), tc.policyEvaluationError }, } - _, err := VerifyAttestations(ctx, resolver, &mockPE, &policy.Policy{ResolvedName: ""}) + _, err := verifyAttestations(ctx, resolver, &mockPE, &policy.Policy{ResolvedName: ""}, &policy.Options{}) if tc.expectedError != nil { if assert.Error(t, err) { assert.Equal(t, tc.expectedError.Error(), err.Error()) @@ -204,16 +206,14 @@ func TestSignVerify(t *testing.T) { keysYaml, err := yaml.Marshal(config) require.NoError(t, err) - // write keysYaml to config.yaml in LocalKeysPolicy. - err = os.WriteFile(filepath.Join(LocalKeysPolicy, "config.yaml"), keysYaml, 0o600) - require.NoError(t, err) - testCases := []struct { name string signTL bool policyDir string imageName string expectedNonSuccess Outcome + spitConfig bool + param string }{ {name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir}, {name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir}, @@ -221,7 +221,9 @@ func TestSignVerify(t *testing.T) { {name: "mirror", signTL: false, policyDir: PassMirrorPolicyDir, imageName: "mirror.org/library/test-image:test"}, {name: "mirror no match", signTL: false, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectedNonSuccess: OutcomeNoPolicy}, {name: "verify inputs", signTL: false, policyDir: InputsPolicyDir}, - {name: "mirror with verification", signTL: false, policyDir: LocalKeysPolicy, imageName: "mirror.org/library/test-image:test"}, + {name: "mirror with verification", signTL: false, policyDir: LocalKeysPolicy, imageName: "mirror.org/library/test-image:test", spitConfig: true}, + {name: "policy with input params", spitConfig: true, signTL: false, policyDir: LocalParamPolicy, param: "bar"}, + {name: "policy without expected param", spitConfig: true, signTL: false, policyDir: LocalParamPolicy, param: "baz", expectedNonSuccess: OutcomeFailure}, } attIdx, err := oci.IndexFromPath(test.UnsignedTestIndex()) @@ -232,6 +234,11 @@ func TestSignVerify(t *testing.T) { if tc.signTL { opts.TransparencyLog = tlog.GetMockTL() } + if tc.spitConfig { + // write keysYaml to config.yaml in LocalKeysPolicy. + err = os.WriteFile(filepath.Join(tc.policyDir, "config.yaml"), keysYaml, 0o600) + require.NoError(t, err) + } signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts) require.NoError(t, err) @@ -254,6 +261,17 @@ func TestSignVerify(t *testing.T) { DisableTUF: true, Debug: true, } + if tc.signTL { + getTL := func(_ context.Context, _ *attestation.VerifyOptions) (tlog.TransparencyLog, error) { + return tlog.GetMockTL(), nil + } + verifier, err := attestation.NewVerfier(attestation.WithLogVerifierFactory(getTL)) + require.NoError(t, err) + policyOpts.AttestationVerifier = verifier + } + if tc.param != "" { + policyOpts.Parameters = policy.Parameters{"foo": tc.param} + } results, err := Verify(ctx, spec, policyOpts) require.NoError(t, err) if tc.expectedNonSuccess != "" {