diff --git a/pkg/webhook/validator.go b/pkg/webhook/validator.go index 6e6872ed2..7f9879e06 100644 --- a/pkg/webhook/validator.go +++ b/pkg/webhook/validator.go @@ -713,6 +713,7 @@ type attestation struct { PredicateType string Payload []byte + Digest string } func attestationToPolicyAttestations(ctx context.Context, atts []attestation) []PolicyAttestation { @@ -743,6 +744,7 @@ func attestationToPolicyAttestations(ctx context.Context, atts []attestation) [] WorkflowRef: ce.GetCertExtensionGithubWorkflowRef(), }, }, + Digest: att.Digest, PredicateType: att.PredicateType, Payload: att.Payload, }) @@ -754,6 +756,7 @@ func attestationToPolicyAttestations(ctx context.Context, atts []attestation) [] }, PredicateType: att.PredicateType, Payload: att.Payload, + Digest: att.Digest, }) } } @@ -890,6 +893,11 @@ func ValidatePolicyAttestationsForAuthority(ctx context.Context, ref name.Refere // attestations and make sure that our particular one is satisfied. checkedAttestations := make([]attestation, 0, len(verifiedAttestations)) for _, va := range verifiedAttestations { + attDigest, err := va.Digest() + if err != nil { + logging.FromContext(ctx).Errorf("failed to get the attestation digest for %s: %v", wantedAttestation.Name, err) + continue + } attBytes, gotPredicateType, err := policy.AttestationToPayloadJSON(ctx, wantedAttestation.PredicateType, va) if gotPredicateType != "" { checkedPredicateTypes[gotPredicateType] = struct{}{} @@ -920,12 +928,15 @@ func ValidatePolicyAttestationsForAuthority(ctx context.Context, ref name.Refere continue } } + + logging.FromContext(ctx).Debugf("found verified attestation with digest: %s", attDigest.String()) // Ok, so this passed aok, jot it down to our result set as // verified attestation with the predicate type match checkedAttestations = append(checkedAttestations, attestation{ Signature: va, PredicateType: wantedAttestation.PredicateType, Payload: attBytes, + Digest: attDigest.String(), }) } if len(checkedAttestations) == 0 { diff --git a/pkg/webhook/validator_result.go b/pkg/webhook/validator_result.go index 6ad998796..d36c3a0fe 100644 --- a/pkg/webhook/validator_result.go +++ b/pkg/webhook/validator_result.go @@ -119,6 +119,9 @@ type PolicyAttestation struct { // not intended for consumption in the ClusterImagePolicy's outer policy // block. Payload []byte `json:"-"` + + // Digest of the attestation + Digest string `json:"digest,omitempty"` } // GithubExtensions holds the Github-related OID extensions. diff --git a/pkg/webhook/validator_test.go b/pkg/webhook/validator_test.go index 25138e493..4080c8160 100644 --- a/pkg/webhook/validator_test.go +++ b/pkg/webhook/validator_test.go @@ -1776,6 +1776,7 @@ func TestValidatePolicy(t *testing.T) { }, }, PredicateType: "vuln", + Digest: "sha256:01bd6aec99ad7c5d045d9aab649fd95b7af2b3b23887d34d7fce8b2e3c38ca0e", Payload: []byte(`{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://cosign.sigstore.dev/attestation/vuln/v1","subject":[{"name":"ghcr.io/distroless/static","digest":{"sha256":"a1e82f6a5f6dfc735165d3442e7cc5a615f72abac3db19452481f5f3c90fbfa8"}}],"predicate":{"invocation":{"parameters":null,"uri":"https://github.com/distroless/static/actions/runs/2757953139","event_id":"2757953139","builder.id":"Create Release"},"scanner":{"uri":"https://github.com/aquasecurity/trivy","version":"0.29.2","db":{"uri":"","version":""},"result":{"$schema":"https://json.schemastore.org/sarif-2.1.0-rtm.5.json","runs":[{"columnKind":"utf16CodeUnits","originalUriBaseIds":{"ROOTPATH":{"uri":"file:///"}},"results":[],"tool":{"driver":{"fullName":"Trivy Vulnerability Scanner","informationUri":"https://github.com/aquasecurity/trivy","name":"Trivy","rules":[],"version":"0.29.2"}}}],"version":"2.1.0"}},"metadata":{"scanStartedOn":"2022-07-29T02:28:42Z","scanFinishedOn":"2022-07-29T02:28:48Z"}}}`), }}, }, @@ -1824,6 +1825,88 @@ func TestValidatePolicy(t *testing.T) { } } +func TestValidatePolicyAttestation(t *testing.T) { + // Resolved via crane digest on 2023/08/08 + digestAtt := name.MustParseReference("ghcr.io/mattmoor/sbom-attestations/spdx-test@sha256:ba4037061b76ad8f306dd9e442877236015747ec42141caf504dc0df4d10708d") + + attPayload := []byte(`{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://spdx.dev/Document","subject":[{"name":"ghcr.io/chainguard-dev/log4shell-demo/app","digest":{"sha256":"ba4037061b76ad8f306dd9e442877236015747ec42141caf504dc0df4d10708d"}}],"predicate":{"Data":{"Reviews":[],"SPDXID":"SPDXRef-SPDXRef-DOCUMENT","annotations":[],"creationInfo":{"comment":"","created":"2022-06-08T15:31:05Z","creators":["Tool: spdx-maven-plugin"],"licenseListVersion":"3.5"},"dataLicense":"CC0-1.0","documentNamespace":"http://spdx.org/spdxpackages/log4shell-1.0-SNAPSHOT","files":[],"hasExtractedLicensingInfos":[],"name":"log4shell","packages":[{"Files":null,"IsFilesAnalyzedTagPresent":true,"IsUnpackaged":false,"SPDXID":"SPDXRef-4","annotations":null,"checksums":null,"comment":"This package was created for a Maven dependency. No SPDX or license information could be found in the Maven POM file.","copyrightText":"UNSPECIFIED","downloadLocation":"NOASSERTION","licenseConcluded":"NOASSERTION","licenseDeclared":"NOASSERTION","licenseInfoFromFiles":["NOASSERTION"],"name":"javax.servlet-api","packageVerificationCode":{"packageVerificationCodeExcludedFiles":null,"packageVerificationCodeValue":""},"versionInfo":"4.0.1"},{"Files":null,"IsFilesAnalyzedTagPresent":true,"IsUnpackaged":false,"SPDXID":"SPDXRef-9","annotations":null,"checksums":null,"comment":"This package was created for a Maven dependency. No SPDX or license information could be found in the Maven POM file.","copyrightText":"UNSPECIFIED","downloadLocation":"NOASSERTION","licenseConcluded":"NOASSERTION","licenseDeclared":"NOASSERTION","licenseInfoFromFiles":["NOASSERTION"],"name":"log4j-api","packageVerificationCode":{"packageVerificationCodeExcludedFiles":null,"packageVerificationCodeValue":""},"versionInfo":"2.14.1"},{"Files":null,"IsFilesAnalyzedTagPresent":true,"IsUnpackaged":false,"SPDXID":"SPDXRef-7","annotations":null,"checksums":null,"comment":"This package was created for a Maven dependency. No SPDX or license information could be found in the Maven POM file.","copyrightText":"UNSPECIFIED","downloadLocation":"NOASSERTION","licenseConcluded":"NOASSERTION","licenseDeclared":"NOASSERTION","licenseInfoFromFiles":["NOASSERTION"],"name":"deploy-jar","packageVerificationCode":{"packageVerificationCodeExcludedFiles":null,"packageVerificationCodeValue":""},"versionInfo":"1.0"},{"Files":null,"IsFilesAnalyzedTagPresent":true,"IsUnpackaged":false,"SPDXID":"SPDXRef-6","annotations":null,"checksums":null,"comment":"This package was created for a Maven dependency. No SPDX or license information could be found in the Maven POM file.","copyrightText":"UNSPECIFIED","downloadLocation":"NOASSERTION","licenseConcluded":"NOASSERTION","licenseDeclared":"NOASSERTION","licenseInfoFromFiles":["NOASSERTION"],"name":"junit-jupiter-engine","packageVerificationCode":{"packageVerificationCodeExcludedFiles":null,"packageVerificationCodeValue":""},"versionInfo":"5.7.1"},{"Files":null,"IsFilesAnalyzedTagPresent":true,"IsUnpackaged":false,"SPDXID":"SPDXRef-8","annotations":null,"checksums":null,"comment":"This package was created for a Maven dependency. No SPDX or license information could be found in the Maven POM file.","copyrightText":"UNSPECIFIED","downloadLocation":"NOASSERTION","licenseConcluded":"NOASSERTION","licenseDeclared":"NOASSERTION","licenseInfoFromFiles":["NOASSERTION"],"name":"log4j-core","packageVerificationCode":{"packageVerificationCodeExcludedFiles":null,"packageVerificationCodeValue":""},"versionInfo":"2.14.1"},{"Files":null,"IsFilesAnalyzedTagPresent":true,"IsUnpackaged":false,"SPDXID":"SPDXRef-5","annotations":null,"checksums":null,"comment":"This package was created for a Maven dependency. No SPDX or license information could be found in the Maven POM file.","copyrightText":"UNSPECIFIED","downloadLocation":"NOASSERTION","licenseConcluded":"NOASSERTION","licenseDeclared":"NOASSERTION","licenseInfoFromFiles":["NOASSERTION"],"name":"junit-jupiter-api","packageVerificationCode":{"packageVerificationCodeExcludedFiles":null,"packageVerificationCodeValue":""},"versionInfo":"5.7.1"},{"Files":[{"SPDXID":"SPDXRef-2","checksums":[{"algorithm":"SHA1","checksumValue":"9e58ba0426bed767f8da4d76afde1ee629d97c41"}],"copyrightText":"http://spdx.org/rdf/terms#noassertion","fileName":"./src/main/java/com/example/log4shell/log4j.java","fileTypes":["source"],"licenseConcluded":"NOASSERTION","licenseInfoInFiles":["NOASSERTION"]},{"SPDXID":"SPDXRef-3","checksums":[{"algorithm":"SHA1","checksumValue":"26df176b1904e473fddc8ca654bce5607b3fc64f"}],"copyrightText":"","fileName":"./src/main/java/com/example/log4shell/LoginServlet.java","fileTypes":["source"],"licenseConcluded":"NOASSERTION","licenseInfoInFiles":["NOASSERTION"]}],"IsFilesAnalyzedTagPresent":true,"IsUnpackaged":false,"SPDXID":"SPDXRef-1","annotations":null,"checksums":null,"copyrightText":"http://spdx.org/rdf/terms#noassertion","downloadLocation":"NOASSERTION","filesAnalyzed":true,"licenseConcluded":"NOASSERTION","licenseDeclared":"NOASSERTION","licenseInfoFromFiles":["NOASSERTION"],"name":"log4shell","packageFileName":"http://spdx.org/rdf/terms#noassertion","packageVerificationCode":{"packageVerificationCodeExcludedFiles":null,"packageVerificationCodeValue":"b5dabb87df1acb05636fe4dbc19afdfe18298a38"},"versionInfo":"1.0-SNAPSHOT"}],"relationships":[{"comment":"Relationship based on Maven POM file dependency information","relatedSpdxElement":"SPDXRef-4","relationshipType":"other","spdxElementId":"SPDXRef-1"},{"comment":"Relationship based on Maven POM file dependency information","relatedSpdxElement":"SPDXRef-9","relationshipType":"dynamicLink","spdxElementId":"SPDXRef-1"},{"comment":"Relationship based on Maven POM file dependency information","relatedSpdxElement":"SPDXRef-7","relationshipType":"other","spdxElementId":"SPDXRef-1"},{"relatedSpdxElement":"SPDXRef-1","relationshipType":"generates","spdxElementId":"SPDXRef-2"},{"comment":"Relationship based on Maven POM file dependency information","relatedSpdxElement":"SPDXRef-6","relationshipType":"testcaseOf","spdxElementId":"SPDXRef-1"},{"relatedSpdxElement":"SPDXRef-1","relationshipType":"generates","spdxElementId":"SPDXRef-3"},{"comment":"Relationship based on Maven POM file dependency information","relatedSpdxElement":"SPDXRef-8","relationshipType":"dynamicLink","spdxElementId":"SPDXRef-1"},{"comment":"Relationship based on Maven POM file dependency information","relatedSpdxElement":"SPDXRef-5","relationshipType":"testcaseOf","spdxElementId":"SPDXRef-1"},{"relatedSpdxElement":"SPDXRef-1","relationshipType":"describes","spdxElementId":"SPDXRef-DOCUMENT"}],"snippets":null,"spdxVersion":"SPDX-2.2"},"Timestamp":""}}`) + + fulcioURL, err := apis.ParseURL("https://fulcio.sigstore.dev") + if err != nil { + t.Fatalf("Failed to parse fake Fulcio URL") + } + + tests := []struct { + name string + policy webhookcip.ClusterImagePolicy + want *PolicyResult + wantErrs []string + customContext context.Context + }{{ + name: "simple test", + policy: webhookcip.ClusterImagePolicy{ + Images: []v1alpha1.ImagePattern{{ + Glob: "**", + }}, + Authorities: []webhookcip.Authority{{ + Name: "authority-0", + Keyless: &webhookcip.KeylessRef{ + URL: fulcioURL, + Identities: []v1alpha1.Identity{{ + IssuerRegExp: ".*", + SubjectRegExp: ".*", + }}, + }, + Attestations: []webhookcip.AttestationPolicy{{ + Name: "test-att", + PredicateType: "https://spdx.dev/Document", + Type: "cue", + Data: `{"predicateType": "https://spdx.dev/Document"}`, + }}, + }, + }, + }, + want: &PolicyResult{ + AuthorityMatches: map[string]AuthorityMatch{ + "authority-0": { + Attestations: map[string][]PolicyAttestation{ + "test-att": {{ + PolicySignature: PolicySignature{ + ID: "2906bbcbb40870d95b19e1bafe1db915ae73e5cd2ae1bdfee539ab6272ae7774", + Subject: "josh@dolit.ski", + Issuer: "https://github.com/login/oauth", + }, + PredicateType: "https://spdx.dev/Document", + Digest: "sha256:f764a4251b2fe3c85dd46896b9d6e65361c9683755099d6dcd13009836d2e0e4", + Payload: attPayload, + }}, + }, + }, + }, + }, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cosignVerifySignatures = cosign.VerifyImageSignatures + cosignVerifyAttestations = cosign.VerifyImageAttestations + testContext := context.Background() + + if test.customContext != nil { + testContext = test.customContext + } + kc, err := k8schain.NewNoClient(testContext) + if err != nil { + t.Fatalf("Failed to construct no client k8schain for testing") + } + got, gotErrs := ValidatePolicy(testContext, system.Namespace(), digestAtt, test.policy, kc) + validateErrors(t, test.wantErrs, gotErrs) + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("unexpected PolicyResult, %s with gotErrs %v", diff, gotErrs) + } + }) + } +} func validateErrors(t *testing.T, wantErr []string, got []error) { t.Helper() if len(wantErr) != len(got) {