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
|
AttestationStyle mapping.AttestationStyle
|
||||||
Debug bool
|
Debug bool
|
||||||
AttestationVerifier attestation.Verifier
|
AttestationVerifier attestation.Verifier
|
||||||
|
// extra parameters to pass through to rego as policy inputs
|
||||||
|
Parameters Parameters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Parameters map[string]string
|
||||||
|
|
||||||
type Policy struct {
|
type Policy struct {
|
||||||
InputFiles []*File
|
InputFiles []*File
|
||||||
Query string
|
Query string
|
||||||
@@ -50,13 +54,14 @@ type Policy struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Input struct {
|
type Input struct {
|
||||||
Digest string `json:"digest"`
|
Digest string `json:"digest"`
|
||||||
PURL string `json:"purl"`
|
PURL string `json:"purl"`
|
||||||
Tag string `json:"tag,omitempty"`
|
Tag string `json:"tag,omitempty"`
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
NormalizedName string `json:"normalized_name"`
|
NormalizedName string `json:"normalized_name"`
|
||||||
FamiliarName string `json:"familiar_name"`
|
FamiliarName string `json:"familiar_name"`
|
||||||
Platform string `json:"platform"`
|
Platform string `json:"platform"`
|
||||||
|
Parameters Parameters `json:"parameters"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type File struct {
|
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)
|
return nil, fmt.Errorf("failed to create attestation resolver: %w", err)
|
||||||
}
|
}
|
||||||
evaluator := policy.NewRegoEvaluator(verifier.opts.Debug, verifier.attestationVerifier)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to evaluate policy: %w", err)
|
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
|
}, 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)
|
desc, err := resolver.ImageDescriptor(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get image descriptor: %w", err)
|
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),
|
Domain: reference.Domain(ref),
|
||||||
NormalizedName: reference.Path(ref),
|
NormalizedName: reference.Path(ref),
|
||||||
FamiliarName: reference.FamiliarName(ref),
|
FamiliarName: reference.FamiliarName(ref),
|
||||||
|
Parameters: opts.Parameters,
|
||||||
}
|
}
|
||||||
// rego has null strings
|
// rego has null strings
|
||||||
if tag != "" {
|
if tag != "" {
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import (
|
|||||||
var (
|
var (
|
||||||
ExampleAttestation = filepath.Join("test", "testdata", "example_attestation.json")
|
ExampleAttestation = filepath.Join("test", "testdata", "example_attestation.json")
|
||||||
LocalKeysPolicy = filepath.Join("test", "testdata", "local-policy-real")
|
LocalKeysPolicy = filepath.Join("test", "testdata", "local-policy-real")
|
||||||
|
LocalParamPolicy = filepath.Join("test", "testdata", "local-policy-param")
|
||||||
|
ExpiresPolicy = filepath.Join("test", "testdata", "expires")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -60,7 +62,7 @@ func TestVerifyAttestations(t *testing.T) {
|
|||||||
return policy.AllowedResult(), tc.policyEvaluationError
|
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 tc.expectedError != nil {
|
||||||
if assert.Error(t, err) {
|
if assert.Error(t, err) {
|
||||||
assert.Equal(t, tc.expectedError.Error(), err.Error())
|
assert.Equal(t, tc.expectedError.Error(), err.Error())
|
||||||
@@ -204,16 +206,14 @@ func TestSignVerify(t *testing.T) {
|
|||||||
keysYaml, err := yaml.Marshal(config)
|
keysYaml, err := yaml.Marshal(config)
|
||||||
require.NoError(t, err)
|
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 {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
signTL bool
|
signTL bool
|
||||||
policyDir string
|
policyDir string
|
||||||
imageName string
|
imageName string
|
||||||
expectedNonSuccess Outcome
|
expectedNonSuccess Outcome
|
||||||
|
spitConfig bool
|
||||||
|
param string
|
||||||
}{
|
}{
|
||||||
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir},
|
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir},
|
||||||
{name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir},
|
{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", 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: "mirror no match", signTL: false, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectedNonSuccess: OutcomeNoPolicy},
|
||||||
{name: "verify inputs", signTL: false, policyDir: InputsPolicyDir},
|
{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())
|
attIdx, err := oci.IndexFromPath(test.UnsignedTestIndex())
|
||||||
@@ -232,6 +234,11 @@ func TestSignVerify(t *testing.T) {
|
|||||||
if tc.signTL {
|
if tc.signTL {
|
||||||
opts.TransparencyLog = tlog.GetMockTL()
|
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)
|
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -254,6 +261,17 @@ func TestSignVerify(t *testing.T) {
|
|||||||
DisableTUF: true,
|
DisableTUF: true,
|
||||||
Debug: 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)
|
results, err := Verify(ctx, spec, policyOpts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
if tc.expectedNonSuccess != "" {
|
if tc.expectedNonSuccess != "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user