Files
attest/pkg/policy/policy.go
2024-08-01 15:24:35 -05:00

259 lines
8.0 KiB
Go

package policy
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"github.com/distribution/reference"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
)
func resolveLocalPolicy(opts *Options, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
if opts.LocalPolicyDir == "" {
return nil, fmt.Errorf("local policy dir not set")
}
files := make([]*File, 0, len(mapping.Files))
for _, f := range mapping.Files {
filename := f.Path
filePath := path.Join(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,
})
}
policy := &Policy{
InputFiles: files,
Mapping: mapping,
}
if imageName != matchedName {
policy.ResolvedName = matchedName
}
return policy, nil
}
func resolveTUFPolicy(opts *Options, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
files := make([]*File, 0, len(mapping.Files))
for _, f := range mapping.Files {
filename := f.Path
_, fileContents, err := opts.TUFClient.DownloadTarget(filename, filepath.Join(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: fileContents,
})
}
policy := &Policy{
InputFiles: files,
Mapping: mapping,
}
if imageName != matchedName {
policy.ResolvedName = matchedName
}
return policy, nil
}
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
}
func resolvePolicyByID(opts *Options) (*Policy, error) {
if opts.PolicyID != "" {
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
if err != nil {
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
}
if localMappings != nil {
policy := localMappings.Policies[opts.PolicyID]
if policy != nil {
return resolveLocalPolicy(opts, policy, "", "")
}
}
// must check tuf
tufMappings, err := config.LoadTUFMappings(opts.TUFClient, opts.LocalTargetsDir)
if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err)
}
policy := tufMappings.Policies[opts.PolicyID]
if policy != nil {
return resolveTUFPolicy(opts, policy, "", "")
}
return nil, fmt.Errorf("policy with id %s not found", opts.PolicyID)
}
return nil, nil
}
func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver, opts *Options) (*Policy, error) {
p, err := resolvePolicyByID(opts)
if err != nil {
return nil, fmt.Errorf("failed to resolve policy by id: %w", err)
}
if p != nil {
return p, nil
}
imageName, err := detailsResolver.ImageName(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get image name: %w", err)
}
imageName, err = normalizeImageName(imageName)
if err != nil {
return nil, fmt.Errorf("failed to parse image name: %w", err)
}
localMappings, err := config.LoadLocalMappings(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 resolveLocalPolicy(opts, match.policy, imageName, match.matchedName)
}
// must check tuf
tufMappings, err := config.LoadTUFMappings(opts.TUFClient, 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 resolveTUFPolicy(opts, 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 resolveTUFPolicy(opts, match.policy, imageName, match.matchedName)
}
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
}
func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsResolver, error) {
switch imageSource.Type {
case oci.OCI:
return oci.NewOCILayoutAttestationResolver(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) (oci.AttestationResolver, error) {
switch resolver := resolver.(type) {
case *oci.RegistryImageDetailsResolver:
if mapping.Attestations != nil && mapping.Attestations.Style == config.AttestationStyleAttached {
return oci.NewRegistryAttestationResolver(resolver)
} else {
if mapping.Attestations != nil && mapping.Attestations.Repo != "" {
return oci.NewReferrersAttestationResolver(resolver, oci.WithReferrersRepo(mapping.Attestations.Repo))
}
return oci.NewReferrersAttestationResolver(resolver)
}
case *oci.LayoutResolver:
if mapping.Attestations != nil {
switch mapping.Attestations.Style {
case config.AttestationStyleAttached:
return resolver, nil
case config.AttestationStyleReferrers:
if mapping.Attestations.Repo != "" {
referrersSpec, err := oci.ParseImageSpec(mapping.Attestations.Repo)
if err != nil {
return nil, fmt.Errorf("failed to parse referrers image spec: %w", err)
}
referrersResolver, err := CreateImageDetailsResolver(referrersSpec)
if err != nil {
return nil, fmt.Errorf("failed to create referrers resolver: %w", err)
}
return oci.NewReferrersAttestationResolver(referrersResolver, oci.WithReferrersRepo(mapping.Attestations.Repo))
}
return oci.NewReferrersAttestationResolver(resolver)
default:
return nil, fmt.Errorf("unsupported attestation style: %s", mapping.Attestations.Style)
}
}
return resolver, nil
default:
return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver)
}
}