feat: support arbitrary rego input parameters (#196)
* feat: support arbitrary rego input parameters
This commit is contained in:
@@ -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 {
|
||||
|
||||
1
test/testdata/local-policy-param/.gitignore
vendored
Normal file
1
test/testdata/local-policy-param/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
config.yaml
|
||||
15
test/testdata/local-policy-param/mapping.yaml
vendored
Normal file
15
test/testdata/local-policy-param/mapping.yaml
vendored
Normal 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
|
||||
61
test/testdata/local-policy-param/policy.rego
vendored
Normal file
61
test/testdata/local-policy-param/policy.rego
vendored
Normal 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",
|
||||
},
|
||||
}
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
Reference in New Issue
Block a user