refactor! remove pkg directory (#145)
* refactor!: remove pkg directory * chore: include breaking changes in draft
This commit is contained in:
2
policy/README.md
Normal file
2
policy/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## policy
|
||||
This package is for attestation policy mapping and evaluation.
|
||||
11
policy/evaluator.go
Normal file
11
policy/evaluator.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/attest/attestation"
|
||||
)
|
||||
|
||||
type Evaluator interface {
|
||||
Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error)
|
||||
}
|
||||
65
policy/match.go
Normal file
65
policy/match.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/attest/config"
|
||||
)
|
||||
|
||||
type matchType string
|
||||
|
||||
const (
|
||||
matchTypePolicy matchType = "policy"
|
||||
matchTypeMatchNoPolicy matchType = "match_no_policy"
|
||||
matchTypeNoMatch matchType = "no_match"
|
||||
)
|
||||
|
||||
type policyMatch struct {
|
||||
matchType matchType
|
||||
policy *config.PolicyMapping
|
||||
rule *config.PolicyRule
|
||||
matchedName string
|
||||
}
|
||||
|
||||
func findPolicyMatch(imageName string, mappings *config.PolicyMappings) (*policyMatch, error) {
|
||||
if mappings == nil {
|
||||
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
|
||||
}
|
||||
return findPolicyMatchImpl(imageName, mappings, make(map[*config.PolicyRule]bool))
|
||||
}
|
||||
|
||||
func findPolicyMatchImpl(imageName string, mappings *config.PolicyMappings, matched map[*config.PolicyRule]bool) (*policyMatch, error) {
|
||||
for _, rule := range mappings.Rules {
|
||||
if rule.Pattern.MatchString(imageName) {
|
||||
switch {
|
||||
case rule.PolicyID == "" && rule.Replacement == "":
|
||||
return nil, fmt.Errorf("rule %s has neither policy-id nor rewrite", rule.Pattern)
|
||||
case rule.PolicyID != "" && rule.Replacement != "":
|
||||
return nil, fmt.Errorf("rule %s has both policy-id and rewrite", rule.Pattern)
|
||||
case rule.PolicyID != "":
|
||||
policy := mappings.Policies[rule.PolicyID]
|
||||
if policy != nil {
|
||||
return &policyMatch{
|
||||
matchType: matchTypePolicy,
|
||||
policy: policy,
|
||||
rule: rule,
|
||||
matchedName: imageName,
|
||||
}, nil
|
||||
}
|
||||
return &policyMatch{
|
||||
matchType: matchTypeMatchNoPolicy,
|
||||
rule: rule,
|
||||
matchedName: imageName,
|
||||
}, nil
|
||||
case rule.Replacement != "":
|
||||
if matched[rule] {
|
||||
return nil, fmt.Errorf("rewrite loop detected")
|
||||
}
|
||||
matched[rule] = true
|
||||
imageName = rule.Pattern.ReplaceAllString(imageName, rule.Replacement)
|
||||
return findPolicyMatchImpl(imageName, mappings, matched)
|
||||
}
|
||||
}
|
||||
}
|
||||
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
|
||||
}
|
||||
121
policy/match_test.go
Normal file
121
policy/match_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFindPolicyMatch(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
imageName string
|
||||
mappingDir string
|
||||
|
||||
expectError bool
|
||||
expectedMatchType matchType
|
||||
expectedPolicyID string
|
||||
expectedImageName string
|
||||
}{
|
||||
{
|
||||
name: "alpine",
|
||||
mappingDir: "doi",
|
||||
imageName: "docker.io/library/alpine",
|
||||
|
||||
expectedMatchType: matchTypePolicy,
|
||||
expectedPolicyID: "docker-official-images",
|
||||
expectedImageName: "docker.io/library/alpine",
|
||||
},
|
||||
{
|
||||
name: "no match",
|
||||
mappingDir: "doi",
|
||||
imageName: "docker.io/something/else",
|
||||
|
||||
expectedMatchType: matchTypeNoMatch,
|
||||
expectedImageName: "docker.io/something/else",
|
||||
},
|
||||
{
|
||||
name: "match, no policy",
|
||||
mappingDir: "local",
|
||||
imageName: "docker.io/library/alpine",
|
||||
|
||||
expectedMatchType: matchTypeMatchNoPolicy,
|
||||
expectedImageName: "docker.io/library/alpine",
|
||||
},
|
||||
{
|
||||
name: "simple rewrite",
|
||||
mappingDir: "simple-rewrite",
|
||||
imageName: "mycoolmirror.org/library/alpine",
|
||||
|
||||
expectedMatchType: matchTypePolicy,
|
||||
expectedPolicyID: "docker-official-images",
|
||||
expectedImageName: "docker.io/library/alpine",
|
||||
},
|
||||
{
|
||||
name: "rewrite no match",
|
||||
mappingDir: "rewrite-to-no-match",
|
||||
imageName: "mycoolmirror.org/library/alpine",
|
||||
|
||||
expectedMatchType: matchTypeNoMatch,
|
||||
expectedImageName: "badredirect.org/alpine",
|
||||
},
|
||||
{
|
||||
name: "rewrite to match, no policy",
|
||||
mappingDir: "rewrite-to-local",
|
||||
imageName: "mycoolmirror.org/library/alpine",
|
||||
|
||||
expectedMatchType: matchTypeMatchNoPolicy,
|
||||
expectedImageName: "docker.io/library/alpine",
|
||||
},
|
||||
{
|
||||
name: "multiple rewrites",
|
||||
mappingDir: "rewrite-multiple",
|
||||
imageName: "myevencoolermirror.org/library/alpine",
|
||||
|
||||
expectedMatchType: matchTypePolicy,
|
||||
expectedPolicyID: "docker-official-images",
|
||||
expectedImageName: "docker.io/library/alpine",
|
||||
},
|
||||
{
|
||||
name: "invalid rewrites",
|
||||
mappingDir: "rewrite-invalid",
|
||||
imageName: "mycoolmirror.org/library/alpine",
|
||||
|
||||
expectError: true,
|
||||
expectedMatchType: matchTypePolicy,
|
||||
expectedPolicyID: "docker-official-images",
|
||||
expectedImageName: "docker.io/library/alpine",
|
||||
},
|
||||
{
|
||||
name: "rewrite loop",
|
||||
mappingDir: "rewrite-loop",
|
||||
imageName: "yin/alpine",
|
||||
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mappings, err := config.LoadLocalMappings(filepath.Join("testdata", "mappings", tc.mappingDir))
|
||||
require.NoError(t, err)
|
||||
match, err := findPolicyMatch(tc.imageName, mappings)
|
||||
if tc.expectError {
|
||||
require.Error(t, err)
|
||||
// TODO: check error matches expected error message
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedMatchType, match.matchType)
|
||||
if match.matchType == matchTypePolicy {
|
||||
if assert.NotNil(t, match.policy) {
|
||||
assert.Equal(t, tc.expectedPolicyID, match.policy.ID)
|
||||
}
|
||||
}
|
||||
assert.Equal(t, tc.expectedImageName, match.matchedName)
|
||||
})
|
||||
}
|
||||
}
|
||||
32
policy/mock.go
Normal file
32
policy/mock.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/attest/attestation"
|
||||
)
|
||||
|
||||
type MockPolicyEvaluator struct {
|
||||
EvaluateFunc func(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error)
|
||||
}
|
||||
|
||||
func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error) {
|
||||
if pe.EvaluateFunc != nil {
|
||||
return pe.EvaluateFunc(ctx, resolver, pctx, input)
|
||||
}
|
||||
return AllowedResult(), nil
|
||||
}
|
||||
|
||||
func GetMockPolicy() Evaluator {
|
||||
return &MockPolicyEvaluator{
|
||||
EvaluateFunc: func(_ context.Context, _ attestation.Resolver, _ *Policy, _ *Input) (*Result, error) {
|
||||
return AllowedResult(), nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func AllowedResult() *Result {
|
||||
return &Result{
|
||||
Success: true,
|
||||
}
|
||||
}
|
||||
38
policy/policy.go
Normal file
38
policy/policy.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/attest/attestation"
|
||||
"github.com/docker/attest/config"
|
||||
"github.com/docker/attest/oci"
|
||||
)
|
||||
|
||||
func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsResolver, error) {
|
||||
switch imageSource.Type {
|
||||
case oci.OCI:
|
||||
return attestation.NewOCILayoutResolver(imageSource)
|
||||
case oci.Docker:
|
||||
return oci.NewRegistryImageDetailsResolver(imageSource)
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported image source type: %s", imageSource.Type)
|
||||
}
|
||||
|
||||
func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (attestation.Resolver, error) {
|
||||
if mapping.Attestations != nil {
|
||||
if mapping.Attestations.Style == config.AttestationStyleAttached {
|
||||
switch resolver := resolver.(type) {
|
||||
case *oci.RegistryImageDetailsResolver:
|
||||
return attestation.NewRegistryResolver(resolver)
|
||||
case *attestation.LayoutResolver:
|
||||
return resolver, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver)
|
||||
}
|
||||
}
|
||||
if mapping.Attestations.Repo != "" {
|
||||
return attestation.NewReferrersResolver(resolver, attestation.WithReferrersRepo(mapping.Attestations.Repo))
|
||||
}
|
||||
}
|
||||
return attestation.NewReferrersResolver(resolver)
|
||||
}
|
||||
175
policy/policy_test.go
Normal file
175
policy/policy_test.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package policy_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/attestation"
|
||||
"github.com/docker/attest/config"
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/oci"
|
||||
"github.com/docker/attest/policy"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func loadAttestation(t *testing.T, path string) *attestation.Envelope {
|
||||
ex, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
env := new(attestation.Envelope)
|
||||
err = json.Unmarshal(ex, env)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
func TestRegoEvaluator_Evaluate(t *testing.T) {
|
||||
ctx, _ := test.Setup(t)
|
||||
resolveErrorStr := "failed to resolve policy by id: policy with id non-existent-policy-id not found"
|
||||
TestDataPath := filepath.Join("..", "test", "testdata")
|
||||
ExampleAttestation := filepath.Join(TestDataPath, "example_attestation.json")
|
||||
|
||||
re := policy.NewRegoEvaluator(true)
|
||||
|
||||
defaultResolver := attestation.MockResolver{
|
||||
Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
policyPath string
|
||||
expectSuccess bool
|
||||
isCanonical bool
|
||||
resolver attestation.Resolver
|
||||
opts *policy.Options
|
||||
policyID string
|
||||
resolveErrorStr string
|
||||
}{
|
||||
{policyPath: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver},
|
||||
{policyPath: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver, policyID: "docker-official-images"},
|
||||
{policyPath: "testdata/policies/allow", resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr},
|
||||
{policyPath: "testdata/policies/deny", resolver: defaultResolver},
|
||||
{policyPath: "testdata/policies/verify-sig", expectSuccess: true, resolver: defaultResolver},
|
||||
{policyPath: "testdata/policies/wrong-key", resolver: defaultResolver},
|
||||
{policyPath: "testdata/policies/allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver},
|
||||
{policyPath: "testdata/policies/allow-canonical", resolver: defaultResolver},
|
||||
{policyPath: "testdata/policies/no-rego", resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.policyPath, func(t *testing.T) {
|
||||
input := &policy.Input{
|
||||
Digest: "sha256:test-digest",
|
||||
PURL: "test-purl",
|
||||
}
|
||||
if !tc.isCanonical {
|
||||
input.Tag = "test"
|
||||
}
|
||||
|
||||
if tc.opts == nil {
|
||||
tc.opts = &policy.Options{
|
||||
LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"),
|
||||
PolicyID: tc.policyID,
|
||||
LocalPolicyDir: tc.policyPath,
|
||||
DisableTUF: true,
|
||||
}
|
||||
}
|
||||
imageName, err := tc.resolver.ImageName(ctx)
|
||||
require.NoError(t, err)
|
||||
resolver := policy.NewResolver(nil, tc.opts)
|
||||
policy, err := resolver.ResolvePolicy(ctx, imageName)
|
||||
if tc.resolveErrorStr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.resolveErrorStr)
|
||||
return
|
||||
}
|
||||
require.NoErrorf(t, err, "failed to resolve policy")
|
||||
require.NotNil(t, policy, "policy should not be nil")
|
||||
result, err := re.Evaluate(ctx, tc.resolver, policy, input)
|
||||
require.NoErrorf(t, err, "Evaluate failed")
|
||||
|
||||
if tc.expectSuccess {
|
||||
assert.True(t, result.Success, "Evaluate should have succeeded")
|
||||
} else {
|
||||
assert.False(t, result.Success, "Evaluate should have failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadingMappings(t *testing.T) {
|
||||
policyMappings, err := config.LoadLocalMappings(filepath.Join("testdata", "policies", "allow"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, len(policyMappings.Rules), 3)
|
||||
for _, mirror := range policyMappings.Rules {
|
||||
if mirror.PolicyID != "" {
|
||||
assert.Equal(t, "docker-official-images", mirror.PolicyID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAttestationResolver(t *testing.T) {
|
||||
mockResolver := attestation.MockResolver{
|
||||
Envs: []*attestation.Envelope{},
|
||||
}
|
||||
layoutResolver := &attestation.LayoutResolver{}
|
||||
registryResolver := &oci.RegistryImageDetailsResolver{}
|
||||
|
||||
nilRepoReferrers := &config.PolicyMapping{
|
||||
Attestations: &config.AttestationConfig{
|
||||
Style: config.AttestationStyleReferrers,
|
||||
},
|
||||
}
|
||||
referrers := &config.PolicyMapping{
|
||||
Attestations: &config.AttestationConfig{
|
||||
Repo: "localhost:5000/repo",
|
||||
Style: config.AttestationStyleReferrers,
|
||||
},
|
||||
}
|
||||
attached := &config.PolicyMapping{
|
||||
Attestations: &config.AttestationConfig{
|
||||
Style: config.AttestationStyleAttached,
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
resolver oci.ImageDetailsResolver
|
||||
mapping *config.PolicyMapping
|
||||
errorStr string
|
||||
}{
|
||||
{name: "referrers", resolver: layoutResolver, mapping: referrers},
|
||||
{name: "referrers (no mapped repo)", resolver: layoutResolver, mapping: nilRepoReferrers},
|
||||
{name: "referrers (no mapping)", resolver: layoutResolver, mapping: &config.PolicyMapping{Attestations: nil}},
|
||||
{name: "attached (registry)", resolver: registryResolver, mapping: attached},
|
||||
{name: "attached (layout)", resolver: layoutResolver, mapping: attached},
|
||||
{name: "attached (unsupported)", resolver: mockResolver, mapping: attached, errorStr: "unsupported image details resolver type"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
resolver, err := policy.CreateAttestationResolver(tc.resolver, tc.mapping)
|
||||
if tc.errorStr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
assert.Contains(t, err.Error(), tc.errorStr)
|
||||
}
|
||||
if tc.mapping.Attestations == nil {
|
||||
return
|
||||
}
|
||||
switch resolver.(type) {
|
||||
case *attestation.ReferrersResolver:
|
||||
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleReferrers)
|
||||
case *attestation.RegistryResolver:
|
||||
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached)
|
||||
case *attestation.LayoutResolver:
|
||||
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
277
policy/rego.go
Normal file
277
policy/rego.go
Normal file
@@ -0,0 +1,277 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/attest/attestation"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
"github.com/open-policy-agent/opa/storage"
|
||||
"github.com/open-policy-agent/opa/storage/inmem"
|
||||
"github.com/open-policy-agent/opa/tester"
|
||||
"github.com/open-policy-agent/opa/topdown"
|
||||
"github.com/open-policy-agent/opa/types"
|
||||
opa "github.com/open-policy-agent/opa/util"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type regoEvaluator struct {
|
||||
debug bool
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultQuery = "result := data.attest.result"
|
||||
resultBinding = "result"
|
||||
)
|
||||
|
||||
func NewRegoEvaluator(debug bool) Evaluator {
|
||||
return ®oEvaluator{
|
||||
debug: debug,
|
||||
}
|
||||
}
|
||||
|
||||
func (re *regoEvaluator) Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error) {
|
||||
var regoOpts []func(*rego.Rego)
|
||||
|
||||
// Create a new in-memory store
|
||||
store := inmem.New()
|
||||
params := storage.TransactionParams{}
|
||||
params.Write = true
|
||||
txn, err := store.NewTransaction(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, target := range pctx.InputFiles {
|
||||
// load yaml as data (no rego opt for this!?)
|
||||
if filepath.Ext(target.Path) == ".yaml" {
|
||||
yamlData, err := loadYAML(target.Path, target.Content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = store.Write(ctx, txn, storage.AddOp, storage.Path{}, yamlData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
regoOpts = append(regoOpts, rego.Module(target.Path, string(target.Content)))
|
||||
}
|
||||
}
|
||||
|
||||
err = store.Commit(ctx, txn)
|
||||
if err != nil {
|
||||
store.Abort(ctx, txn)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if re.debug {
|
||||
regoOpts = append(regoOpts,
|
||||
rego.EnablePrintStatements(true),
|
||||
rego.PrintHook(topdown.NewPrintHook(os.Stderr)),
|
||||
rego.Dump(os.Stderr),
|
||||
)
|
||||
}
|
||||
query := DefaultQuery
|
||||
if pctx.Query != "" {
|
||||
query = pctx.Query
|
||||
}
|
||||
regoOpts = append(regoOpts,
|
||||
rego.Query(query),
|
||||
rego.Input(input),
|
||||
rego.Store(store),
|
||||
rego.GenerateJSON(jsonGenerator[Result]()),
|
||||
)
|
||||
for _, custom := range RegoFunctions(resolver) {
|
||||
regoOpts = append(regoOpts, custom.Func)
|
||||
}
|
||||
|
||||
r := rego.New(regoOpts...)
|
||||
rs, err := r.Eval(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(rs) == 0 {
|
||||
return nil, fmt.Errorf("no policy evaluation result")
|
||||
}
|
||||
binding, ok := rs[0].Bindings[resultBinding]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to extract verification result")
|
||||
}
|
||||
result, ok := binding.(Result)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to extract verification result")
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func jsonGenerator[T any]() func(t *ast.Term, ec *rego.EvalContext) (any, error) {
|
||||
return func(t *ast.Term, _ *rego.EvalContext) (any, error) {
|
||||
// TODO: this is horrible - we're converting the AST to JSON and then back to AST, then using ast.As to convert it to a struct
|
||||
// We can't use ast.As directly because it fails if the AST contains a set
|
||||
json, err := ast.JSON(t.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, err := ast.InterfaceToValue(json)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result T
|
||||
err = ast.As(v, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
var dynamicObj = types.NewObject(nil, types.NewDynamicProperty(types.A, types.A))
|
||||
|
||||
var verifyDecl = &ast.Builtin{
|
||||
Name: "attest.verify",
|
||||
Decl: types.NewFunction(types.Args(dynamicObj, dynamicObj), dynamicObj),
|
||||
Nondeterministic: true,
|
||||
}
|
||||
|
||||
var attestDecl = &ast.Builtin{
|
||||
Name: "attest.fetch",
|
||||
Decl: types.NewFunction(types.Args(types.S), dynamicObj),
|
||||
Nondeterministic: true,
|
||||
}
|
||||
|
||||
func wrapFunctionResult(value *ast.Term, err error) (*ast.Term, error) {
|
||||
var terms [][2]*ast.Term
|
||||
if err != nil {
|
||||
terms = append(terms, [2]*ast.Term{ast.StringTerm("error"), ast.StringTerm(err.Error())})
|
||||
}
|
||||
if value != nil {
|
||||
terms = append(terms, [2]*ast.Term{ast.StringTerm("value"), value})
|
||||
}
|
||||
return ast.ObjectTerm(terms...), nil
|
||||
}
|
||||
|
||||
func handleErrors1(f func(rCtx rego.BuiltinContext, a *ast.Term) (*ast.Term, error)) rego.Builtin1 {
|
||||
return func(rCtx rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
|
||||
return wrapFunctionResult(f(rCtx, a))
|
||||
}
|
||||
}
|
||||
|
||||
func handleErrors2(f func(rCtx *rego.BuiltinContext, a, b *ast.Term) (*ast.Term, error)) rego.Builtin2 {
|
||||
return func(rCtx rego.BuiltinContext, a, b *ast.Term) (*ast.Term, error) {
|
||||
return wrapFunctionResult(f(&rCtx, a, b))
|
||||
}
|
||||
}
|
||||
|
||||
func RegoFunctions(resolver attestation.Resolver) []*tester.Builtin {
|
||||
return []*tester.Builtin{
|
||||
{
|
||||
Decl: verifyDecl,
|
||||
Func: rego.Function2(
|
||||
®o.Function{
|
||||
Name: verifyDecl.Name,
|
||||
Decl: verifyDecl.Decl,
|
||||
Memoize: true,
|
||||
Nondeterministic: verifyDecl.Nondeterministic,
|
||||
},
|
||||
handleErrors2(verifyInTotoEnvelope)),
|
||||
},
|
||||
{
|
||||
Decl: attestDecl,
|
||||
Func: rego.Function1(
|
||||
®o.Function{
|
||||
Name: attestDecl.Name,
|
||||
Decl: attestDecl.Decl,
|
||||
Memoize: true,
|
||||
Nondeterministic: attestDecl.Nondeterministic,
|
||||
},
|
||||
handleErrors1(fetchInTotoAttestations(resolver))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func fetchInTotoAttestations(resolver attestation.Resolver) rego.Builtin1 {
|
||||
return func(rCtx rego.BuiltinContext, predicateTypeTerm *ast.Term) (*ast.Term, error) {
|
||||
predicateTypeStr, ok := predicateTypeTerm.Value.(ast.String)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("predicateTypeTerm is not a string")
|
||||
}
|
||||
predicateType := string(predicateTypeStr)
|
||||
|
||||
envelopes, err := resolver.Attestations(rCtx.Context, predicateType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert each envelope to an ast.Value.
|
||||
values := make([]*ast.Term, len(envelopes))
|
||||
for i, envelope := range envelopes {
|
||||
value, err := ast.InterfaceToValue(envelope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values[i] = ast.NewTerm(value)
|
||||
}
|
||||
|
||||
// Wrap the values in an ast.Set and convert it to an ast.Term.
|
||||
set := ast.NewTerm(ast.NewSet(values...))
|
||||
|
||||
return set, nil
|
||||
}
|
||||
}
|
||||
|
||||
func verifyInTotoEnvelope(rCtx *rego.BuiltinContext, envTerm, optsTerm *ast.Term) (*ast.Term, error) {
|
||||
env := new(attestation.Envelope)
|
||||
opts := new(attestation.VerifyOptions)
|
||||
err := ast.As(envTerm.Value, env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to cast envelope: %w", err)
|
||||
}
|
||||
err = ast.As(optsTerm.Value, &opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to cast verifier options: %w", err)
|
||||
}
|
||||
|
||||
payload, err := attestation.VerifyDSSE(rCtx.Context, env, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
statement := new(intoto.Statement)
|
||||
|
||||
switch env.PayloadType {
|
||||
case intoto.PayloadType:
|
||||
err = json.Unmarshal(payload, statement)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal statement: %w", err)
|
||||
}
|
||||
// TODO: implement other types of envelope
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported payload type: %s", env.PayloadType)
|
||||
}
|
||||
|
||||
value, err := ast.InterfaceToValue(statement)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ast.NewTerm(value), nil
|
||||
}
|
||||
|
||||
func loadYAML(path string, bs []byte) (interface{}, error) {
|
||||
var x interface{}
|
||||
bs, err := yaml.YAMLToJSON(bs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v: error converting YAML to JSON: %v", path, err)
|
||||
}
|
||||
err = opa.UnmarshalJSON(bs, &x)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
194
policy/resolver.go
Normal file
194
policy/resolver.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/attest/config"
|
||||
"github.com/docker/attest/internal/util"
|
||||
"github.com/docker/attest/tuf"
|
||||
)
|
||||
|
||||
type Resolver struct {
|
||||
tufClient tuf.Downloader
|
||||
opts *Options
|
||||
}
|
||||
|
||||
func NewResolver(tufClient tuf.Downloader, opts *Options) *Resolver {
|
||||
return &Resolver{
|
||||
tufClient: tufClient,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resolver) ResolvePolicy(_ context.Context, imageName string) (*Policy, error) {
|
||||
p, err := r.resolvePolicyByID()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve policy by id: %w", err)
|
||||
}
|
||||
if p != nil {
|
||||
return p, nil
|
||||
}
|
||||
imageName, err = normalizeImageName(imageName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse image name: %w", err)
|
||||
}
|
||||
localMappings, err := config.LoadLocalMappings(r.opts.LocalPolicyDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
||||
}
|
||||
match, err := findPolicyMatch(imageName, localMappings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if match.matchType == matchTypePolicy {
|
||||
return r.resolveLocalPolicy(match.policy, imageName, match.matchedName)
|
||||
}
|
||||
if !r.opts.DisableTUF {
|
||||
tufMappings, err := config.LoadTUFMappings(r.tufClient, r.opts.LocalTargetsDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err)
|
||||
}
|
||||
|
||||
// it's a mirror of a tuf policy
|
||||
if match.matchType == matchTypeMatchNoPolicy {
|
||||
for _, mapping := range tufMappings.Policies {
|
||||
if mapping.ID == match.rule.PolicyID {
|
||||
return r.resolveTUFPolicy(mapping, imageName, match.matchedName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try to resolve a tuf policy directly
|
||||
match, err = findPolicyMatch(imageName, tufMappings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if match.matchType == matchTypePolicy {
|
||||
return r.resolveTUFPolicy(match.policy, imageName, match.matchedName)
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) resolveLocalPolicy(mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
|
||||
if r.opts.LocalPolicyDir == "" {
|
||||
return nil, fmt.Errorf("local policy dir not set")
|
||||
}
|
||||
var URI string
|
||||
var digest map[string]string
|
||||
files := make([]*File, 0, len(mapping.Files))
|
||||
for _, f := range mapping.Files {
|
||||
filename := f.Path
|
||||
filePath := path.Join(r.opts.LocalPolicyDir, filename)
|
||||
fileContents, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read policy file %s: %w", filename, err)
|
||||
}
|
||||
files = append(files, &File{
|
||||
Path: filename,
|
||||
Content: fileContents,
|
||||
})
|
||||
// if the file is a policy file, store the URI and digest
|
||||
if filepath.Ext(filename) == ".rego" {
|
||||
// TODO: support multiple rego files, need some way to identify the main policy file
|
||||
if URI != "" {
|
||||
return nil, fmt.Errorf("multiple policy files found in policy mapping")
|
||||
}
|
||||
URI = filePath
|
||||
digest = map[string]string{"sha256": util.SHA256Hex(fileContents)}
|
||||
}
|
||||
}
|
||||
if URI == "" {
|
||||
return nil, fmt.Errorf("no policy file found in policy mapping")
|
||||
}
|
||||
policy := &Policy{
|
||||
InputFiles: files,
|
||||
Mapping: mapping,
|
||||
URI: URI,
|
||||
Digest: digest,
|
||||
}
|
||||
if imageName != matchedName {
|
||||
policy.ResolvedName = matchedName
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) resolveTUFPolicy(mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
|
||||
var URI string
|
||||
var digest map[string]string
|
||||
files := make([]*File, 0, len(mapping.Files))
|
||||
for _, f := range mapping.Files {
|
||||
filename := f.Path
|
||||
file, err := r.tufClient.DownloadTarget(filename, filepath.Join(r.opts.LocalTargetsDir, filename))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err)
|
||||
}
|
||||
files = append(files, &File{
|
||||
Path: filename,
|
||||
Content: file.Data,
|
||||
})
|
||||
// if the file is a policy file, store the URI and digest
|
||||
if filepath.Ext(filename) == ".rego" {
|
||||
// TODO: support multiple rego files, need some way to identify the main policy file
|
||||
if URI != "" {
|
||||
return nil, fmt.Errorf("multiple policy files found in policy mapping")
|
||||
}
|
||||
URI = file.TargetURI
|
||||
digest = map[string]string{"sha256": file.Digest}
|
||||
}
|
||||
}
|
||||
if URI == "" {
|
||||
return nil, fmt.Errorf("no policy file found in policy mapping")
|
||||
}
|
||||
policy := &Policy{
|
||||
InputFiles: files,
|
||||
Mapping: mapping,
|
||||
URI: URI,
|
||||
Digest: digest,
|
||||
}
|
||||
if imageName != matchedName {
|
||||
policy.ResolvedName = matchedName
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) resolvePolicyByID() (*Policy, error) {
|
||||
if r.opts.PolicyID != "" {
|
||||
localMappings, err := config.LoadLocalMappings(r.opts.LocalPolicyDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
||||
}
|
||||
if localMappings != nil {
|
||||
policy := localMappings.Policies[r.opts.PolicyID]
|
||||
if policy != nil {
|
||||
return r.resolveLocalPolicy(policy, "", "")
|
||||
}
|
||||
}
|
||||
|
||||
if !r.opts.DisableTUF {
|
||||
tufMappings, err := config.LoadTUFMappings(r.tufClient, r.opts.LocalTargetsDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err)
|
||||
}
|
||||
policy := tufMappings.Policies[r.opts.PolicyID]
|
||||
if policy != nil {
|
||||
return r.resolveTUFPolicy(policy, "", "")
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("policy with id %s not found", r.opts.PolicyID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func normalizeImageName(imageName string) (string, error) {
|
||||
named, err := reference.ParseNormalizedNamed(imageName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse image name: %w", err)
|
||||
}
|
||||
return named.Name(), nil
|
||||
}
|
||||
65
policy/resolver_test.go
Normal file
65
policy/resolver_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package policy_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/policy"
|
||||
"github.com/docker/attest/tuf"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResolvePolicy(t *testing.T) {
|
||||
localPolicyPath := "testdata/policies/allow"
|
||||
tufPolicyPath := "testdata/policies/allow-canonical"
|
||||
noLocalPolicyPath := "testdata/policies/no-policy"
|
||||
testPolicyID := "docker-official-images"
|
||||
testImageName := "localhost:5001/test/repo:tag"
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
policyPath string
|
||||
policyID string
|
||||
localOverridesTUF bool // if a policy is provided locally, it should override TUF
|
||||
DisableTUF bool
|
||||
}{
|
||||
{name: "resolve by id (TUF only)", policyID: testPolicyID, DisableTUF: false},
|
||||
{name: "resolve by id (local mapping, TUF policy)", policyPath: noLocalPolicyPath, policyID: testPolicyID, DisableTUF: false},
|
||||
{name: "resolve by id (local mapping, local policy, no TUF)", policyPath: localPolicyPath, policyID: testPolicyID, DisableTUF: true},
|
||||
{name: "resolve by id (local mapping, local policy)", policyPath: localPolicyPath, policyID: testPolicyID, DisableTUF: false, localOverridesTUF: true},
|
||||
{name: "resolve by match (TUF only)", DisableTUF: false},
|
||||
{name: "resolve by match (local mapping, TUF policy)", policyPath: noLocalPolicyPath, DisableTUF: false},
|
||||
{name: "resolve by match (local mapping, local policy, no TUF)", policyPath: localPolicyPath, DisableTUF: true},
|
||||
{name: "resolve by match (local mapping, local policy)", policyPath: localPolicyPath, DisableTUF: false, localOverridesTUF: true},
|
||||
}
|
||||
|
||||
var tufClient tuf.Downloader
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
opts := &policy.Options{}
|
||||
tempDir := test.CreateTempDir(t, "", "tuf-dest")
|
||||
if !tc.DisableTUF {
|
||||
tufClient = tuf.NewMockTufClient(tufPolicyPath)
|
||||
}
|
||||
if tc.policyID != "" {
|
||||
opts.PolicyID = tc.policyID
|
||||
}
|
||||
if tc.policyPath != "" {
|
||||
opts.LocalPolicyDir = tc.policyPath
|
||||
}
|
||||
opts.DisableTUF = tc.DisableTUF
|
||||
opts.LocalTargetsDir = tempDir
|
||||
resolver := policy.NewResolver(tufClient, opts)
|
||||
policy, err := resolver.ResolvePolicy(context.Background(), testImageName)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, policy)
|
||||
if tc.DisableTUF || tc.localOverridesTUF {
|
||||
assert.Contains(t, policy.URI, localPolicyPath)
|
||||
} else {
|
||||
assert.Contains(t, policy.URI, tufPolicyPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
10
policy/testdata/mappings/doi/mapping.yaml
vendored
Normal file
10
policy/testdata/mappings/doi/mapping.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
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
|
||||
10
policy/testdata/mappings/local/mapping.yaml
vendored
Normal file
10
policy/testdata/mappings/local/mapping.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
version: v1
|
||||
kind: policy-mapping
|
||||
policies:
|
||||
- id: local-policy
|
||||
description: Local Policy
|
||||
files:
|
||||
- path: local-policy.rego
|
||||
rules:
|
||||
- pattern: "^docker[.]io/library/(.*)$"
|
||||
policy-id: docker-official-images # note this policy does not exist in this file
|
||||
13
policy/testdata/mappings/rewrite-invalid/mapping.yaml
vendored
Normal file
13
policy/testdata/mappings/rewrite-invalid/mapping.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
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: "^mycoolmirror[.]org/library/(.*)$"
|
||||
rewrite: "docker.io/library/$1"
|
||||
policy-id: docker-official-images # invalid to specify both rewrite and policy-id
|
||||
14
policy/testdata/mappings/rewrite-loop/mapping.yaml
vendored
Normal file
14
policy/testdata/mappings/rewrite-loop/mapping.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
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: "^yin/(.*)$"
|
||||
rewrite: "yang/$1"
|
||||
- pattern: "^yang/(.*)$"
|
||||
rewrite: "yin/$1"
|
||||
14
policy/testdata/mappings/rewrite-multiple/mapping.yaml
vendored
Normal file
14
policy/testdata/mappings/rewrite-multiple/mapping.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
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: "^mycoolmirror[.]org/library/(.*)$"
|
||||
rewrite: "docker.io/library/$1"
|
||||
- pattern: "^myevencoolermirror[.]org/library/(.*)$"
|
||||
rewrite: "mycoolmirror.org/library/$1"
|
||||
12
policy/testdata/mappings/rewrite-to-local/mapping.yaml
vendored
Normal file
12
policy/testdata/mappings/rewrite-to-local/mapping.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
version: v1
|
||||
kind: policy-mapping
|
||||
policies:
|
||||
- id: local-policy
|
||||
description: Local Policy
|
||||
files:
|
||||
- path: local-policy.rego
|
||||
rules:
|
||||
- pattern: "^docker[.]io/library/(.*)$"
|
||||
policy-id: docker-official-images # note this policy does not exist in this file
|
||||
- pattern: "^mycoolmirror[.]org/library/(.*)$"
|
||||
rewrite: "docker.io/library/$1"
|
||||
12
policy/testdata/mappings/rewrite-to-no-match/mapping.yaml
vendored
Normal file
12
policy/testdata/mappings/rewrite-to-no-match/mapping.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
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: "^mycoolmirror[.]org/library/(.*)$"
|
||||
rewrite: "badredirect.org/$1" # no matching rule for this rewrite
|
||||
12
policy/testdata/mappings/simple-rewrite/mapping.yaml
vendored
Normal file
12
policy/testdata/mappings/simple-rewrite/mapping.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
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: "^mycoolmirror[.]org/library/(.*)$"
|
||||
rewrite: "docker.io/library/$1"
|
||||
11
policy/testdata/policies/allow-canonical/doi/policy.rego
vendored
Normal file
11
policy/testdata/policies/allow-canonical/doi/policy.rego
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
package attest
|
||||
|
||||
import rego.v1
|
||||
|
||||
default canonical = false
|
||||
|
||||
canonical if {
|
||||
not input.tag
|
||||
}
|
||||
|
||||
result := {"success": canonical}
|
||||
17
policy/testdata/policies/allow-canonical/mapping.yaml
vendored
Normal file
17
policy/testdata/policies/allow-canonical/mapping.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# map repos to policies
|
||||
version: v1
|
||||
kind: policy-mapping
|
||||
policies:
|
||||
- id: docker-official-images
|
||||
description: Docker Official Images
|
||||
attestations:
|
||||
repo: "localhost:5001/library-refs"
|
||||
files:
|
||||
- path: doi/policy.rego
|
||||
rules:
|
||||
- pattern: "^docker[.]io/library/(.*)$"
|
||||
policy-id: docker-official-images
|
||||
- pattern: ^localhost:5001/(.*)$
|
||||
rewrite: docker.io/library/$1
|
||||
- pattern: ^registry[.]local:5000/(.*)$
|
||||
rewrite: docker.io/library/$1
|
||||
7
policy/testdata/policies/allow/doi/policy.rego
vendored
Normal file
7
policy/testdata/policies/allow/doi/policy.rego
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
package attest
|
||||
|
||||
import rego.v1
|
||||
|
||||
result := {
|
||||
"success": true,
|
||||
}
|
||||
17
policy/testdata/policies/allow/mapping.yaml
vendored
Normal file
17
policy/testdata/policies/allow/mapping.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# map repos to policies
|
||||
version: v1
|
||||
kind: policy-mapping
|
||||
policies:
|
||||
- id: docker-official-images
|
||||
description: Docker Official Images
|
||||
attestations:
|
||||
repo: "localhost:5001/library-refs"
|
||||
files:
|
||||
- path: doi/policy.rego
|
||||
rules:
|
||||
- pattern: "^docker[.]io/library/(.*)$"
|
||||
policy-id: docker-official-images
|
||||
- pattern: ^localhost:5001/(.*)$
|
||||
rewrite: docker.io/library/$1
|
||||
- pattern: ^registry[.]local:5000/(.*)$
|
||||
rewrite: docker.io/library/$1
|
||||
7
policy/testdata/policies/deny/doi/policy.rego
vendored
Normal file
7
policy/testdata/policies/deny/doi/policy.rego
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
package attest
|
||||
|
||||
import rego.v1
|
||||
|
||||
result := {
|
||||
"success": false,
|
||||
}
|
||||
11
policy/testdata/policies/deny/mapping.yaml
vendored
Normal file
11
policy/testdata/policies/deny/mapping.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# map repos to policies
|
||||
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
|
||||
10
policy/testdata/policies/no-policy/mapping.yaml
vendored
Normal file
10
policy/testdata/policies/no-policy/mapping.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# map repos to policies
|
||||
version: v1
|
||||
kind: policy-mapping
|
||||
rules:
|
||||
- pattern: "^docker[.]io/library/(.*)$"
|
||||
policy-id: docker-official-images
|
||||
- pattern: ^localhost:5001/(.*)$
|
||||
rewrite: docker.io/library/$1
|
||||
- pattern: ^registry[.]local:5000/(.*)$
|
||||
rewrite: docker.io/library/$1
|
||||
1
policy/testdata/policies/no-rego/doi/policy.yaml
vendored
Normal file
1
policy/testdata/policies/no-rego/doi/policy.yaml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
policy: "this is not rego"
|
||||
11
policy/testdata/policies/no-rego/mapping.yaml
vendored
Normal file
11
policy/testdata/policies/no-rego/mapping.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# map repos to policies
|
||||
version: v1
|
||||
kind: policy-mapping
|
||||
policies:
|
||||
- id: docker-official-images
|
||||
description: Docker Official Images
|
||||
files:
|
||||
- path: doi/policy.yaml
|
||||
rules:
|
||||
- pattern: "^docker[.]io/library/(.*)$"
|
||||
policy-id: docker-official-images
|
||||
19
policy/testdata/policies/verify-sig/doi/policy.rego
vendored
Normal file
19
policy/testdata/policies/verify-sig/doi/policy.rego
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
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,
|
||||
}]
|
||||
|
||||
opts := {"keys": keys}
|
||||
|
||||
success if {
|
||||
some env in attest.fetch("foo")
|
||||
statement := attest.verify(env, opts)
|
||||
}
|
||||
|
||||
result := {"success": success}
|
||||
11
policy/testdata/policies/verify-sig/mapping.yaml
vendored
Normal file
11
policy/testdata/policies/verify-sig/mapping.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# map repos to policies
|
||||
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
|
||||
30
policy/testdata/policies/wrong-key/doi/policy.rego
vendored
Normal file
30
policy/testdata/policies/wrong-key/doi/policy.rego
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
package attest
|
||||
|
||||
import rego.v1
|
||||
|
||||
keys := [{
|
||||
"id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4",
|
||||
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHyZpSgzvqFqNv7f3x7865OS38rAb\nQMcff55zM2UH/KR3Pr84a8QsGDNgaNGzJQJWjtMSgfV8WnNoffNK+svFNg==\n-----END PUBLIC KEY-----",
|
||||
"from": "2023-12-15T14:00:00Z",
|
||||
"to": null,
|
||||
}]
|
||||
|
||||
default success := false
|
||||
|
||||
provs(pred) := p if {
|
||||
res := attest.fetch(pred)
|
||||
not res.error
|
||||
p := res.value
|
||||
}
|
||||
|
||||
atts := union({provs("foo")})
|
||||
|
||||
opts := {"keys": keys}
|
||||
|
||||
success if {
|
||||
some env in atts
|
||||
res := attest.verify(env, opts)
|
||||
not res.error
|
||||
}
|
||||
|
||||
result := {"success": success}
|
||||
11
policy/testdata/policies/wrong-key/mapping.yaml
vendored
Normal file
11
policy/testdata/policies/wrong-key/mapping.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# map repos to policies
|
||||
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
|
||||
62
policy/types.go
Normal file
62
policy/types.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"github.com/docker/attest/config"
|
||||
"github.com/docker/attest/tuf"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
)
|
||||
|
||||
type Summary struct {
|
||||
Subjects []intoto.Subject `json:"subjects"`
|
||||
SLSALevels []string `json:"slsa_levels"`
|
||||
Verifier string `json:"verifier"`
|
||||
PolicyURI string `json:"policy_uri"`
|
||||
}
|
||||
|
||||
type Violation struct {
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Attestation *intoto.Statement `json:"attestation"`
|
||||
Details map[string]any `json:"details"`
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Success bool `json:"success"`
|
||||
Violations []Violation `json:"violations"`
|
||||
Summary Summary `json:"summary"`
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
TUFClientOptions *tuf.ClientOptions
|
||||
DisableTUF bool
|
||||
LocalTargetsDir string
|
||||
LocalPolicyDir string
|
||||
PolicyID string
|
||||
ReferrersRepo string
|
||||
AttestationStyle config.AttestationStyle
|
||||
Debug bool
|
||||
}
|
||||
|
||||
type Policy struct {
|
||||
InputFiles []*File
|
||||
Query string
|
||||
Mapping *config.PolicyMapping
|
||||
ResolvedName string
|
||||
URI string
|
||||
Digest map[string]string
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Path string
|
||||
Content []byte
|
||||
}
|
||||
Reference in New Issue
Block a user