Files
attest/policy/policy_test.go
2024-10-18 09:25:31 -05:00

430 lines
12 KiB
Go

/*
Copyright Docker attest authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package policy_test
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/docker/attest/attestation"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/mapping"
"github.com/docker/attest/oci"
"github.com/docker/attest/policy"
v1 "github.com/google/go-containerregistry/pkg/v1"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/package-url/packageurl-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func loadAttestation(t *testing.T, path string) *attestation.EnvelopeReference {
ex, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
env := new(attestation.EnvelopeReference)
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")
verifier, err := attestation.NewVerfier()
require.NoError(t, err)
re := policy.NewRegoEvaluator(true, verifier)
defaultResolver := attestation.MockResolver{
Envs: []*attestation.EnvelopeReference{loadAttestation(t, ExampleAttestation)},
}
defaultPlatform, err := v1.ParsePlatform("linux/amd64")
require.NoError(t, err)
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, defaultPlatform)
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 := mapping.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.EnvelopeReference{},
}
layoutResolver := &attestation.LayoutResolver{}
registryResolver := &oci.RegistryImageDetailsResolver{}
nilRepoReferrers := &mapping.PolicyMapping{
Attestations: &mapping.AttestationConfig{
Style: mapping.AttestationStyleReferrers,
},
}
referrers := &mapping.PolicyMapping{
Attestations: &mapping.AttestationConfig{
Repo: "localhost:5000/repo",
Style: mapping.AttestationStyleReferrers,
},
}
attached := &mapping.PolicyMapping{
Attestations: &mapping.AttestationConfig{
Style: mapping.AttestationStyleAttached,
},
}
testCases := []struct {
name string
resolver oci.ImageDetailsResolver
mapping *mapping.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: &mapping.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, mapping.AttestationStyleReferrers)
case *attestation.RegistryResolver:
assert.Equal(t, tc.mapping.Attestations.Style, mapping.AttestationStyleAttached)
case *attestation.LayoutResolver:
assert.Equal(t, tc.mapping.Attestations.Style, mapping.AttestationStyleAttached)
}
})
}
}
func TestVerifySubject(t *testing.T) {
ctx, _ := test.Setup(t)
defaultResolver := attestation.MockResolver{}
hostWithPort := packageurl.QualifiersFromMap(map[string]string{"platform": "linux/amd64"})
withHost := packageurl.NewPackageURL(packageurl.TypeDocker, "localhost:1234", "alpine", "", hostWithPort, "")
testCases := []struct {
name string
subject []intoto.Subject
img string
expectError bool
digest string
}{
{
name: "library short",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "alpine",
},
{
name: "with domain and namespace",
subject: []intoto.Subject{
{
Name: "pkg:docker/docker.io/library/alpine@latest?platform=linux%2Famd64",
},
},
img: "alpine",
},
{
name: "with host and port",
subject: []intoto.Subject{
{
Name: withHost.ToString(),
},
},
img: "localhost:1234/alpine",
},
{
name: "with host and port (from image-signer-verifier tests)",
subject: []intoto.Subject{
{
Name: "pkg:docker/registry.local%3A5000/image-signer-verifier-test@10710107227?platform=linux%2Famd64",
},
},
img: "registry.local:5000/image-signer-verifier-test",
},
{
name: "with library",
subject: []intoto.Subject{
{
Name: "pkg:docker/library/alpine@latest?platform=linux%2Famd64",
},
},
img: "alpine",
},
{
name: "library short with tag",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "alpine:foo",
},
{
name: "library with namespace",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "library/alpine:foo",
},
{
name: "library with domain",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "docker.io/library/alpine:foo",
},
{
name: "domain mismatch",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "ecr.io/library/alpine:foo",
expectError: true,
},
{
name: "type mismatch",
subject: []intoto.Subject{
{
Name: "pkg:node/alpine@latest?platform=linux%2Famd64",
},
},
img: "alpine",
expectError: true,
},
{
name: "name mismatch",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "library/debian:latest",
expectError: true,
},
{
name: "namespace mismatch",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "unsupported/alpine:latest",
expectError: true,
},
{
name: "digest mismatch",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "alpine",
digest: "1234",
expectError: true,
},
{
name: "platform mismatch",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Farm64",
},
},
img: "alpine",
expectError: true,
},
{
name: "malformed purl",
subject: []intoto.Subject{
{
Name: "not-a-purl",
},
},
img: "alpine",
expectError: true,
},
{
name: "malformed image in valid purl",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine,bar@latest?platform=linux%2Famd64",
},
},
img: "alpine-broken",
expectError: true,
},
{
name: "malformed image name",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "foo bar",
expectError: true,
},
}
digestHex := strings.TrimPrefix(test.UnsignedLinuxAMD64ImageDigest, "sha256:")
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
defaultResolver.Image = tc.img
// make sure we're using a fixed platform vs a detected one
defaultResolver.PlatformFn = func() (*v1.Platform, error) {
return &v1.Platform{Architecture: "amd64", OS: "linux"}, nil
}
// digest from mock resolver
tc.subject[0].Digest = map[string]string{"sha256": digestHex}
if tc.digest != "" {
tc.subject[0].Digest = map[string]string{"sha256": tc.digest}
}
err := policy.VerifySubject(ctx, tc.subject, defaultResolver)
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
defaultResolver.Image = "alpine"
subject := []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
Digest: map[string]string{"sha256": digestHex},
},
}
// error getting descriptor
defaultResolver.DescriptorFn = func() (*v1.Descriptor, error) {
return nil, fmt.Errorf("error")
}
err := policy.VerifySubject(ctx, subject, defaultResolver)
require.Error(t, err)
// error getting platform
defaultResolver.DescriptorFn = nil
defaultResolver.PlatformFn = func() (*v1.Platform, error) {
return nil, fmt.Errorf("error")
}
err = policy.VerifySubject(ctx, subject, defaultResolver)
require.Error(t, err)
// error getting image name
defaultResolver.PlatformFn = nil
defaultResolver.Image = ""
defaultResolver.ImangeNameFn = func() (string, error) {
return "", fmt.Errorf("error")
}
err = policy.VerifySubject(ctx, subject, defaultResolver)
require.Error(t, err)
}