feat: add purl details to policy inputs (#129)
This commit is contained in:
@@ -128,7 +128,10 @@ The input to the policy is an object with the following fields:
|
|||||||
|
|
||||||
- `digest` (string): the digest of the image being verified
|
- `digest` (string): the digest of the image being verified
|
||||||
- `purl` (string): the package URL of the image being verified
|
- `purl` (string): the package URL of the image being verified
|
||||||
- `is_canonical` (bool): whether the image being verified was referenced by a 'canonical' name, i.e. one that contains a digest
|
- `platform` (string): the platform of the image being verified
|
||||||
|
- `normalized_name` (string): defaults are filled out. e.g. if the image is `alpine`, this would be `library/alpine`
|
||||||
|
- `familiar_name` (string): short version of above (e.g. `alpine`)
|
||||||
|
- `tag`: (string): tag of the image being verified (if present)
|
||||||
|
|
||||||
### Builtin Functions
|
### Builtin Functions
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ var (
|
|||||||
PassMirrorPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-mirror")
|
PassMirrorPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-mirror")
|
||||||
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
|
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
|
||||||
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
|
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
|
||||||
|
InputsPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-inputs")
|
||||||
TestTempDir = "attest-sign-test"
|
TestTempDir = "attest-sign-test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -140,14 +140,34 @@ func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, pctx
|
|||||||
name = strings.Replace(name, oldName, pctx.ResolvedName, 1)
|
name = strings.Replace(name, oldName, pctx.ResolvedName, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
purl, canonical, err := oci.RefToPURL(name, platform)
|
ref, err := reference.ParseNormalizedNamed(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse ref %q: %w", ref, err)
|
||||||
|
}
|
||||||
|
purl, canonical, err := oci.RefToPURL(ref, platform)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to convert ref to purl: %w", err)
|
return nil, fmt.Errorf("failed to convert ref to purl: %w", err)
|
||||||
}
|
}
|
||||||
|
var tag string
|
||||||
|
if !canonical {
|
||||||
|
// unlike the function name indicates, this adds latest if no tag is present
|
||||||
|
ref = reference.TagNameOnly(ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tagged, ok := ref.(reference.Tagged); ok {
|
||||||
|
tag = tagged.Tag()
|
||||||
|
}
|
||||||
input := &policy.Input{
|
input := &policy.Input{
|
||||||
Digest: digest,
|
Digest: digest,
|
||||||
PURL: purl,
|
PURL: purl,
|
||||||
IsCanonical: canonical,
|
Platform: platform.String(),
|
||||||
|
Domain: reference.Domain(ref),
|
||||||
|
NormalizedName: reference.Path(ref),
|
||||||
|
FamiliarName: reference.FamiliarName(ref),
|
||||||
|
}
|
||||||
|
// rego has null strings
|
||||||
|
if tag != "" {
|
||||||
|
input.Tag = tag
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluator, err := policy.GetPolicyEvaluator(ctx)
|
evaluator, err := policy.GetPolicyEvaluator(ctx)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/attest/internal/test"
|
"github.com/docker/attest/internal/test"
|
||||||
"github.com/docker/attest/pkg/attestation"
|
"github.com/docker/attest/pkg/attestation"
|
||||||
"github.com/docker/attest/pkg/config"
|
"github.com/docker/attest/pkg/config"
|
||||||
@@ -98,7 +99,7 @@ func TestVSA(t *testing.T) {
|
|||||||
|
|
||||||
if assert.NotNil(t, results.Input) {
|
if assert.NotNil(t, results.Input) {
|
||||||
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", results.Input.Digest)
|
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", results.Input.Digest)
|
||||||
assert.False(t, results.Input.IsCanonical)
|
assert.NotNil(t, results.Input.Tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, intoto.StatementInTotoV01, results.VSA.Type)
|
assert.Equal(t, intoto.StatementInTotoV01, results.VSA.Type)
|
||||||
@@ -187,6 +188,7 @@ func TestSignVerify(t *testing.T) {
|
|||||||
{name: "no tl", signTL: false, policyDir: PassPolicyDir},
|
{name: "no tl", signTL: false, policyDir: PassPolicyDir},
|
||||||
{name: "mirror", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "mirror.org/library/test-image:test"},
|
{name: "mirror", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "mirror.org/library/test-image:test"},
|
||||||
{name: "mirror no match", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectError: true},
|
{name: "mirror no match", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectError: true},
|
||||||
|
{name: "verify inputs", signTL: false, policyDir: InputsPolicyDir},
|
||||||
}
|
}
|
||||||
|
|
||||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||||
@@ -226,7 +228,10 @@ func TestSignVerify(t *testing.T) {
|
|||||||
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
||||||
platform, err := oci.ParsePlatform(LinuxAMD64)
|
platform, err := oci.ParsePlatform(LinuxAMD64)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
expectedPURL, _, err := oci.RefToPURL(attIdx.Name, platform)
|
|
||||||
|
ref, err := reference.ParseNormalizedNamed(attIdx.Name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
expectedPURL, _, err := oci.RefToPURL(ref, platform)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, expectedPURL, results.Input.PURL)
|
assert.Equal(t, expectedPURL, results.Input.PURL)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -52,12 +52,8 @@ func ImageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descripto
|
|||||||
return nil, fmt.Errorf("no image found for platform %v", platform)
|
return nil, fmt.Errorf("no image found for platform %v", platform)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RefToPURL(ref string, platform *v1.Platform) (string, bool, error) {
|
func RefToPURL(named reference.Named, platform *v1.Platform) (string, bool, error) {
|
||||||
var isCanonical bool
|
var isCanonical bool
|
||||||
named, err := reference.ParseNormalizedNamed(ref)
|
|
||||||
if err != nil {
|
|
||||||
return "", false, fmt.Errorf("failed to parse ref %q: %w", ref, err)
|
|
||||||
}
|
|
||||||
var qualifiers []packageurl.Qualifier
|
var qualifiers []packageurl.Qualifier
|
||||||
|
|
||||||
if canonical, ok := named.(reference.Canonical); ok {
|
if canonical, ok := named.(reference.Canonical); ok {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package oci_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/attest/internal/test"
|
"github.com/docker/attest/internal/test"
|
||||||
"github.com/docker/attest/pkg/oci"
|
"github.com/docker/attest/pkg/oci"
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
@@ -14,42 +15,51 @@ import (
|
|||||||
func TestRefToPurl(t *testing.T) {
|
func TestRefToPurl(t *testing.T) {
|
||||||
arm, err := oci.ParsePlatform("arm64/linux")
|
arm, err := oci.ParsePlatform("arm64/linux")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
purl, canonical, err := oci.RefToPURL("alpine", arm)
|
ref, err := reference.ParseNormalizedNamed("alpine")
|
||||||
|
require.NoError(t, err)
|
||||||
|
purl, canonical, err := oci.RefToPURL(ref, arm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "pkg:docker/alpine@latest?platform=arm64%2Flinux", purl)
|
assert.Equal(t, "pkg:docker/alpine@latest?platform=arm64%2Flinux", purl)
|
||||||
assert.False(t, canonical)
|
assert.False(t, canonical)
|
||||||
|
ref, err = reference.ParseNormalizedNamed("alpine:123")
|
||||||
purl, canonical, err = oci.RefToPURL("alpine:123", arm)
|
require.NoError(t, err)
|
||||||
|
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
||||||
assert.False(t, canonical)
|
assert.False(t, canonical)
|
||||||
|
ref, err = reference.ParseNormalizedNamed("google/alpine:123")
|
||||||
purl, canonical, err = oci.RefToPURL("google/alpine:123", arm)
|
require.NoError(t, err)
|
||||||
|
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "pkg:docker/google/alpine@123?platform=arm64%2Flinux", purl)
|
assert.Equal(t, "pkg:docker/google/alpine@123?platform=arm64%2Flinux", purl)
|
||||||
assert.False(t, canonical)
|
assert.False(t, canonical)
|
||||||
|
ref, err = reference.ParseNormalizedNamed("library/alpine:123")
|
||||||
purl, canonical, err = oci.RefToPURL("library/alpine:123", arm)
|
require.NoError(t, err)
|
||||||
|
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
||||||
assert.False(t, canonical)
|
assert.False(t, canonical)
|
||||||
|
ref, err = reference.ParseNormalizedNamed("docker.io/library/alpine:123")
|
||||||
purl, canonical, err = oci.RefToPURL("docker.io/library/alpine:123", arm)
|
require.NoError(t, err)
|
||||||
|
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
||||||
assert.False(t, canonical)
|
assert.False(t, canonical)
|
||||||
|
ref, err = reference.ParseNormalizedNamed("localhost:5001/library/alpine:123")
|
||||||
purl, canonical, err = oci.RefToPURL("localhost:5001/library/alpine:123", arm)
|
require.NoError(t, err)
|
||||||
|
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "pkg:docker/localhost%3A5001/library/alpine@123?platform=arm64%2Flinux", purl)
|
assert.Equal(t, "pkg:docker/localhost%3A5001/library/alpine@123?platform=arm64%2Flinux", purl)
|
||||||
assert.False(t, canonical)
|
assert.False(t, canonical)
|
||||||
|
ref, err = reference.ParseNormalizedNamed("localhost:5001/alpine:123")
|
||||||
purl, canonical, err = oci.RefToPURL("localhost:5001/alpine:123", arm)
|
require.NoError(t, err)
|
||||||
|
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine@123?platform=arm64%2Flinux", purl)
|
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine@123?platform=arm64%2Flinux", purl)
|
||||||
assert.False(t, canonical)
|
assert.False(t, canonical)
|
||||||
|
ref, err = reference.ParseNormalizedNamed("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b")
|
||||||
purl, canonical, err = oci.RefToPURL("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b", arm)
|
require.NoError(t, err)
|
||||||
|
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine?digest=sha256%3Ac5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b&platform=arm64%2Flinux", purl)
|
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine?digest=sha256%3Ac5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b&platform=arm64%2Flinux", purl)
|
||||||
assert.True(t, canonical)
|
assert.True(t, canonical)
|
||||||
|
|||||||
@@ -51,23 +51,25 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
|||||||
policyID string
|
policyID string
|
||||||
resolveErrorStr string
|
resolveErrorStr string
|
||||||
}{
|
}{
|
||||||
{repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver},
|
{repo: "testdata/mock-tuf-allow", expectSuccess: true, resolver: defaultResolver},
|
||||||
{repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver, policyID: "docker-official-images"},
|
{repo: "testdata/mock-tuf-allow", expectSuccess: true, resolver: defaultResolver, policyID: "docker-official-images"},
|
||||||
{repo: "testdata/mock-tuf-allow", expectSuccess: false, isCanonical: false, resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr},
|
{repo: "testdata/mock-tuf-allow", resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr},
|
||||||
{repo: "testdata/mock-tuf-deny", expectSuccess: false, isCanonical: false, resolver: defaultResolver},
|
{repo: "testdata/mock-tuf-deny", resolver: defaultResolver},
|
||||||
{repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, isCanonical: false, resolver: defaultResolver},
|
{repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, resolver: defaultResolver},
|
||||||
{repo: "testdata/mock-tuf-wrong-key", expectSuccess: false, isCanonical: false, resolver: defaultResolver},
|
{repo: "testdata/mock-tuf-wrong-key", resolver: defaultResolver},
|
||||||
{repo: "testdata/mock-tuf-allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver},
|
{repo: "testdata/mock-tuf-allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver},
|
||||||
{repo: "testdata/mock-tuf-allow-canonical", expectSuccess: false, isCanonical: false, resolver: defaultResolver},
|
{repo: "testdata/mock-tuf-allow-canonical", resolver: defaultResolver},
|
||||||
{repo: "testdata/mock-tuf-no-rego", expectSuccess: false, isCanonical: false, resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"},
|
{repo: "testdata/mock-tuf-no-rego", resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.repo, func(t *testing.T) {
|
t.Run(tc.repo, func(t *testing.T) {
|
||||||
input := &policy.Input{
|
input := &policy.Input{
|
||||||
Digest: "sha256:test-digest",
|
Digest: "sha256:test-digest",
|
||||||
PURL: "test-purl",
|
PURL: "test-purl",
|
||||||
IsCanonical: tc.isCanonical,
|
}
|
||||||
|
if !tc.isCanonical {
|
||||||
|
input.Tag = "test"
|
||||||
}
|
}
|
||||||
|
|
||||||
tufClient := tuf.NewMockTufClient(tc.repo, test.CreateTempDir(t, "", "tuf-dest"))
|
tufClient := tuf.NewMockTufClient(tc.repo, test.CreateTempDir(t, "", "tuf-dest"))
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ package attest
|
|||||||
|
|
||||||
import rego.v1
|
import rego.v1
|
||||||
|
|
||||||
result := {
|
default canonical = false
|
||||||
"success": input.isCanonical,
|
|
||||||
|
canonical if {
|
||||||
|
not input.tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result := {"success": canonical}
|
||||||
|
|||||||
@@ -45,9 +45,13 @@ 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"`
|
||||||
IsCanonical bool `json:"isCanonical"`
|
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 {
|
type File struct {
|
||||||
|
|||||||
43
test/testdata/local-policy-inputs/doi/policy.rego
vendored
Normal file
43
test/testdata/local-policy-inputs/doi/policy.rego
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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,
|
||||||
|
"status": "active",
|
||||||
|
"signing-format": "dssev1",
|
||||||
|
}]
|
||||||
|
|
||||||
|
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"),
|
||||||
|
})
|
||||||
|
|
||||||
|
success if {
|
||||||
|
input.domain == "docker.io"
|
||||||
|
input.familiar_name == "test-image"
|
||||||
|
input.normalized_name == "library/test-image"
|
||||||
|
input.platform == "linux/amd64"
|
||||||
|
input.tag == "test"
|
||||||
|
}
|
||||||
|
|
||||||
|
result := {
|
||||||
|
"success": success,
|
||||||
|
"violations": set(),
|
||||||
|
"attestations": set(),
|
||||||
|
"summary": {
|
||||||
|
"subjects": set(),
|
||||||
|
"slsa_level": "SLSA_BUILD_LEVEL_3",
|
||||||
|
"verifier": "docker-official-images",
|
||||||
|
"policy_uri": "https://docker.com/official/policy/v0.1",
|
||||||
|
},
|
||||||
|
}
|
||||||
18
test/testdata/local-policy-inputs/mapping.yaml
vendored
Normal file
18
test/testdata/local-policy-inputs/mapping.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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: "repo$"
|
||||||
|
policy-id: docker-official-images
|
||||||
|
- pattern: "test-image$"
|
||||||
|
policy-id: docker-official-images
|
||||||
|
- pattern: "image-signer-verifier-test$"
|
||||||
|
policy-id: docker-official-images
|
||||||
|
- pattern: "library/(.*)$"
|
||||||
|
rewrite: docker.io/library/$1
|
||||||
@@ -38,7 +38,7 @@ subjects contains subject if {
|
|||||||
}
|
}
|
||||||
|
|
||||||
success if {
|
success if {
|
||||||
print("input:",input)
|
# print("input:",input)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user