feat: support arbitrary rego input parameters (#196)

* feat: support arbitrary rego input parameters
This commit is contained in:
James Carnegie
2024-10-15 16:07:26 +01:00
committed by GitHub
parent 7027d2d054
commit da667de610
6 changed files with 116 additions and 15 deletions

View File

@@ -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 {

View File

@@ -0,0 +1 @@
config.yaml

View File

@@ -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

View File

@@ -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",
},
}

View File

@@ -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 != "" {

View File

@@ -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 != "" {