diff --git a/pkg/cosign/cosign.go b/pkg/cosign/cosign.go index 4d97d60477..c609368c8b 100644 --- a/pkg/cosign/cosign.go +++ b/pkg/cosign/cosign.go @@ -13,7 +13,6 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/in-toto/in-toto-golang/in_toto" wildcard "github.com/kyverno/go-wildcard" - kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" "github.com/kyverno/kyverno/pkg/registryclient" "github.com/kyverno/kyverno/pkg/utils" "github.com/pkg/errors" @@ -35,6 +34,7 @@ var ImageSignatureRepository string type Options struct { ImageRef string + FetchAttestations bool Key string Cert string CertChain string @@ -47,30 +47,95 @@ type Options struct { RekorURL string } -// VerifySignature verifies that the image has the expected signatures -func VerifySignature(opts Options) (digest string, err error) { - ctx := context.Background() - var remoteOpts []remote.Option - ro := options.RegistryOptions{} - remoteOpts, err = ro.ClientOpts(ctx) - if err != nil { - return "", errors.Wrap(err, "constructing client options") +type Response struct { + Digest string + Statements []map[string]interface{} +} + +func Verify(opts Options) (*Response, error) { + if opts.FetchAttestations { + return fetchAttestations(opts) + } else { + return verifySignature(opts) } +} + +// verifySignature verifies that the image has the expected signatures +func verifySignature(opts Options) (*Response, error) { + ref, err := name.ParseReference(opts.ImageRef) + if err != nil { + return nil, fmt.Errorf("failed to parse image %s", opts.ImageRef) + } + + cosignOpts, err := buildCosignOptions(opts) + if err != nil { + return nil, err + } + + signatures, bundleVerified, err := client.VerifyImageSignatures(context.Background(), ref, cosignOpts) + if err != nil { + logger.Info("image verification failed", "error", err.Error()) + return nil, err + } + + logger.V(3).Info("verified image", "count", len(signatures), "bundleVerified", bundleVerified) + payload, err := extractPayload(signatures) + if err != nil { + return nil, err + } + + if err := matchSubjectAndIssuer(signatures, opts.Subject, opts.Issuer); err != nil { + return nil, err + } + + if err := matchExtensions(signatures, opts.AdditionalExtensions); err != nil { + return nil, err + } + + err = checkAnnotations(payload, opts.Annotations) + if err != nil { + return nil, err + } + + digest, err := extractDigest(opts.ImageRef, payload) + if err != nil { + return nil, err + } + + return &Response{Digest: digest}, nil +} + +func buildCosignOptions(opts Options) (*cosign.CheckOpts, error) { + var remoteOpts []remote.Option + var err error + ro := options.RegistryOptions{} + remoteOpts, err = ro.ClientOpts(context.Background()) + if err != nil { + return nil, errors.Wrap(err, "constructing client options") + } + o, err := registryclient.GetOptions() if err != nil { - return "", errors.Wrap(err, "getting remote options") + return nil, errors.Wrap(err, "getting remote options") } + remoteOpts = append(remoteOpts, remote.WithRemoteOptions(o)) + cosignOpts := &cosign.CheckOpts{ Annotations: map[string]interface{}{}, RegistryClientOpts: remoteOpts, - ClaimVerifier: cosign.SimpleClaimVerifier, + } + + if opts.FetchAttestations { + cosignOpts.ClaimVerifier = cosign.IntotoSubjectClaimVerifier + } else { + cosignOpts.ClaimVerifier = cosign.SimpleClaimVerifier } if opts.Roots != "" { cp, err := loadCertPool([]byte(opts.Roots)) if err != nil { - return "", errors.Wrapf(err, "failed to load Root certificates") + return nil, errors.Wrap(err, "failed to load Root certificates") } cosignOpts.RootCerts = cp } @@ -79,13 +144,13 @@ func VerifySignature(opts Options) (digest string, err error) { if strings.HasPrefix(opts.Key, "-----BEGIN PUBLIC KEY-----") { cosignOpts.SigVerifier, err = decodePEM([]byte(opts.Key)) if err != nil { - return "", errors.Wrap(err, "failed to load public key from PEM") + return nil, errors.Wrap(err, "failed to load public key from PEM") } } else { // this supports Kubernetes secrets and KMS - cosignOpts.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, opts.Key) + cosignOpts.SigVerifier, err = sigs.PublicKeyFromKeyRef(context.Background(), opts.Key) if err != nil { - return "", errors.Wrapf(err, "failed to load public key from %s", opts.Key) + return nil, errors.Wrapf(err, "failed to load public key from %s", opts.Key) } } } else { @@ -93,30 +158,30 @@ func VerifySignature(opts Options) (digest string, err error) { // load cert and optionally a cert chain as a verifier cert, err := loadCert([]byte(opts.Cert)) if err != nil { - return "", errors.Wrapf(err, "failed to load certificate from %s", opts.Cert) + return nil, errors.Wrapf(err, "failed to load certificate from %s", string(opts.Cert)) } if opts.CertChain == "" { cosignOpts.SigVerifier, err = signature.LoadVerifier(cert.PublicKey, crypto.SHA256) if err != nil { - return "", errors.Wrapf(err, "failed to load signature from certificate") + return nil, errors.Wrap(err, "failed to load signature from certificate") } } else { // Verify certificate with chain chain, err := loadCertChain([]byte(opts.CertChain)) if err != nil { - return "", err + return nil, errors.Wrap(err, "failed to load load certificate chain") } cosignOpts.SigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, cosignOpts) if err != nil { - return "", err + return nil, errors.Wrap(err, "failed to load validate certificate chain") } } } else if opts.CertChain != "" { // load cert chain as roots cp, err := loadCertPool([]byte(opts.CertChain)) if err != nil { - return "", errors.Wrapf(err, "failed to load cert chain") + return nil, errors.Wrap(err, "failed to load certificates") } cosignOpts.RootCerts = cp } else { @@ -130,63 +195,20 @@ func VerifySignature(opts Options) (digest string, err error) { if opts.RekorURL != "" { cosignOpts.RekorClient, err = rekor.NewClient(opts.RekorURL) if err != nil { - return "", errors.Wrapf(err, "failed to create Rekor client from URL %s", opts.RekorURL) + return nil, errors.Wrapf(err, "failed to create Rekor client from URL %s", opts.RekorURL) } } if opts.Repository != "" { signatureRepo, err := name.NewRepository(opts.Repository) if err != nil { - return "", errors.Wrapf(err, "failed to parse signature repository %s", opts.Repository) + return nil, errors.Wrapf(err, "failed to parse signature repository %s", opts.Repository) } cosignOpts.RegistryClientOpts = append(cosignOpts.RegistryClientOpts, remote.WithTargetRepository(signatureRepo)) } - ref, err := name.ParseReference(opts.ImageRef) - if err != nil { - return "", errors.Wrap(err, "failed to parse image") - } - - signatures, bundleVerified, err := client.VerifyImageSignatures(ctx, ref, cosignOpts) - if err != nil { - logger.Info("image verification failed", "error", err.Error()) - return "", err - } - - logger.V(3).Info("verified image", "count", len(signatures), "bundleVerified", bundleVerified) - pld, err := extractPayload(signatures) - if err != nil { - return "", errors.Wrap(err, "failed to get pld") - } - - if err := matchSubjectAndIssuer(signatures, opts.Subject, opts.Issuer); err != nil { - return "", err - } - - if err := matchExtensions(signatures, opts.AdditionalExtensions); err != nil { - return "", errors.Wrap(err, "extensions mismatch") - } - - err = checkAnnotations(pld, opts.Annotations) - if err != nil { - return "", errors.Wrap(err, "annotation mismatch") - } - - digest, err = extractDigest(opts.ImageRef, pld) - if err != nil { - return "", errors.Wrap(err, "failed to get digest") - } - - return digest, nil -} - -func getFulcioRoots(roots []byte) (*x509.CertPool, error) { - if len(roots) == 0 { - return fulcio.GetRoots(), nil - } - - return loadCertPool(roots) + return cosignOpts, nil } func loadCertPool(roots []byte) (*x509.CertPool, error) { @@ -220,35 +242,15 @@ func loadCertChain(pem []byte) ([]*x509.Certificate, error) { return cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(pem)) } -// FetchAttestations retrieves signed attestations and decodes them into in-toto statements +// fetchAttestations retrieves signed attestations and decodes them into in-toto statements // https://github.com/in-toto/attestation/blob/main/spec/README.md#statement -func FetchAttestations(imageRef string, imageVerify kyvernov1.ImageVerification) ([]map[string]interface{}, error) { - ctx := context.Background() - var err error - - cosignOpts := &cosign.CheckOpts{ - ClaimVerifier: cosign.IntotoSubjectClaimVerifier, - } - - if imageVerify.Key != "" { - if strings.HasPrefix(imageVerify.Key, "-----BEGIN PUBLIC KEY-----") { - cosignOpts.SigVerifier, err = decodePEM([]byte(imageVerify.Key)) - } else { - cosignOpts.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, imageVerify.Key) - } - } else { - cosignOpts.CertEmail = "" - cosignOpts.RootCerts, err = getFulcioRoots([]byte(imageVerify.Roots)) - if err == nil { - cosignOpts.RekorClient, err = rekor.NewClient("https://rekor.sigstore.dev") - } - } - +func fetchAttestations(opts Options) (*Response, error) { + cosignOpts, err := buildCosignOptions(opts) if err != nil { - return nil, errors.Wrap(err, "loading credentials") + return nil, err } - ref, err := name.ParseReference(imageRef) + ref, err := name.ParseReference(opts.ImageRef) if err != nil { return nil, errors.Wrap(err, "failed to parse image") } @@ -258,51 +260,61 @@ func FetchAttestations(imageRef string, imageVerify kyvernov1.ImageVerification) msg := err.Error() logger.Info("failed to fetch attestations", "error", msg) if strings.Contains(msg, "MANIFEST_UNKNOWN: manifest unknown") { - return nil, fmt.Errorf("not found") + return nil, errors.Wrap(fmt.Errorf("not found"), "") } return nil, err } - logger.V(3).Info("verified images", "count", len(signatures), "bundleVerified", bundleVerified) - inTotoStatements, err := decodeStatements(signatures) + logger.V(3).Info("verified images", "signatures", len(signatures), "bundleVerified", bundleVerified) + inTotoStatements, digest, err := decodeStatements(signatures) if err != nil { return nil, err } - return inTotoStatements, nil + return &Response{Digest: digest, Statements: inTotoStatements}, nil } -func decodeStatements(sigs []oci.Signature) ([]map[string]interface{}, error) { +func decodeStatements(sigs []oci.Signature) ([]map[string]interface{}, string, error) { if len(sigs) == 0 { - return []map[string]interface{}{}, nil + return []map[string]interface{}{}, "", nil } + var digest string decodedStatements := make([]map[string]interface{}, len(sigs)) for i, sig := range sigs { pld, err := sig.Payload() if err != nil { - return nil, errors.Wrap(err, "failed to decode payload") + return nil, "", errors.Wrap(err, "failed to decode payload") + } + + sci := payload.SimpleContainerImage{} + if err := json.Unmarshal(pld, &sci); err != nil { + return nil, "", errors.Wrap(err, "error decoding the payload") + } + + if d := sci.Critical.Image.DockerManifestDigest; d != "" { + digest = d } data := make(map[string]interface{}) if err := json.Unmarshal(pld, &data); err != nil { - return nil, errors.Wrapf(err, "failed to unmarshal JSON payload: %v", sig) + return nil, "", errors.Wrapf(err, "failed to unmarshal JSON payload: %v", sig) } if dataPayload, ok := data["payload"]; !ok { - return nil, fmt.Errorf("missing payload in %v", data) + return nil, "", fmt.Errorf("missing payload in %v", data) } else { decodedStatement, err := decodeStatement(dataPayload.(string)) if err != nil { - return nil, errors.Wrapf(err, "failed to decode statement %s", string(pld)) + return nil, "", errors.Wrapf(err, "failed to decode statement %s", string(pld)) } decodedStatements[i] = decodedStatement } } - return decodedStatements, nil + return decodedStatements, digest, nil } func decodeStatement(payloadBase64 string) (map[string]interface{}, error) { @@ -482,7 +494,8 @@ func checkAnnotations(payload []payload.SimpleContainerImage, annotations map[st for _, p := range payload { for key, val := range annotations { if val != p.Optional[key] { - return fmt.Errorf("annotation value for %s does not match", key) + return fmt.Errorf("annotations mismatch: %s does not match expected value %s for key %s", + p.Optional[key], val, key) } } } diff --git a/pkg/cosign/cosign_test.go b/pkg/cosign/cosign_test.go index a2bcd612bb..297c57232e 100644 --- a/pkg/cosign/cosign_test.go +++ b/pkg/cosign/cosign_test.go @@ -70,14 +70,14 @@ func TestCosignKeyless(t *testing.T) { Subject: "jim", } - _, err := VerifySignature(opts) + _, err := verifySignature(opts) assert.Error(t, err, "subject mismatch: expected jim@nirmata.com, got jim") opts.Subject = "jim@nirmata.com" - _, err = VerifySignature(opts) + _, err = verifySignature(opts) assert.Error(t, err, "issuer mismatch: expected https://github.com/login/oauth, got https://github.com/") opts.Issuer = "https://github.com/login/oauth" - _, err = VerifySignature(opts) + _, err = verifySignature(opts) assert.NilError(t, err) } diff --git a/pkg/engine/attestation_test.go b/pkg/engine/attestation_test.go new file mode 100644 index 0000000000..ad540f0361 --- /dev/null +++ b/pkg/engine/attestation_test.go @@ -0,0 +1,264 @@ +package engine + +import ( + "encoding/json" + "testing" + + v1 "github.com/kyverno/kyverno/api/kyverno/v1" + "github.com/kyverno/kyverno/pkg/engine/context" + "github.com/kyverno/kyverno/pkg/utils/api" + "github.com/kyverno/kyverno/pkg/utils/image" + "gotest.tools/assert" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +var scanPredicate = ` +{ + "predicate": { + "matches": [ + { + "vulnerability": { + "id": "CVE-2021-22946", + "dataSource": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-22946", + "namespace": "alpine:3.11", + "severity": "High", + "urls": [ + "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-22946" + ], + "cvss": [], + "fix": { + "versions": [ + "7.79.0-r0" + ], + "state": "fixed" + }, + "advisories": [] + }, + "relatedVulnerabilities": [ + { + "id": "CVE-2021-22946", + "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2021-22946", + "namespace": "nvd", + "severity": "High", + "urls": [ + "https://hackerone.com/reports/1334111", + "https://lists.debian.org/debian-lts-announce/2021/09/msg00022.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RWLEC6YVEM2HWUBX67SDGPSY4CQB72OE/", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://security.netapp.com/advisory/ntap-20211029-0003/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/APOAK4X73EJTAPTSVT7IRVDMUWVXNWGD/", + "https://security.netapp.com/advisory/ntap-20220121-0008/", + "https://www.oracle.com/security-alerts/cpujan2022.html", + "https://cert-portal.siemens.com/productcert/pdf/ssa-389290.pdf", + "https://support.apple.com/kb/HT213183", + "http://seclists.org/fulldisclosure/2022/Mar/29", + "https://www.oracle.com/security-alerts/cpuapr2022.html" + ], + "description": "A user can tell curl >= 7.20.0 and <= 7.78.0 to require a successful upgrade to TLS when speaking to an IMAP, POP3 or FTP server (--ssl-reqd on the command line or CURLOPT_USE_SSL set to CURLUSESSL_CONTROL or CURLUSESSL_ALL withlibcurl). This requirement could be bypassed if the server would return a properly crafted but perfectly legitimate response.This flaw would then make curl silently continue its operations **withoutTLS** contrary to the instructions and expectations, exposing possibly sensitive data in clear text over the network.", + "cvss": [ + { + "version": "2.0", + "vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "metrics": { + "baseScore": 5, + "exploitabilityScore": 10, + "impactScore": 2.9 + }, + "vendorMetadata": {} + }, + { + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + "metrics": { + "baseScore": 7.5, + "exploitabilityScore": 3.9, + "impactScore": 3.6 + }, + "vendorMetadata": {} + } + ] + } + ], + "matchDetails": [ + { + "matcher": "apk-matcher", + "searchedBy": { + "distro": { + "type": "alpine", + "version": "3.11.7" + }, + "namespace": "alpine:3.11", + "package": { + "name": "curl", + "version": "7.67.0-r3" + } + }, + "found": { + "versionConstraint": "< 7.79.0-r0 (apk)" + } + } + ], + "artifact": { + "name": "libcurl", + "version": "7.67.0-r3", + "type": "apk", + "locations": [ + { + "path": "/lib/apk/db/installed", + "layerID": "sha256:165c22a332e306497ffa210ce9f284906fe0bf6340d20c5f8521e064323ba52a" + } + ], + "language": "", + "licenses": [ + "MIT" + ], + "cpes": [ + "cpe:2.3:a:libcurl:libcurl:7.67.0-r3:*:*:*:*:*:*:*" + ], + "purl": "pkg:alpine/libcurl@7.67.0-r3?arch=x86_64", + "metadata": { + "OriginPackage": "curl" + } + } + } + ], + "source": { + "type": "image", + "target": { + "userInput": "ghcr.io/tap8stry/git-init:v0.21.0@sha256:322e3502c1e6fba5f1869efb55cfd998a3679e073840d33eb0e3c482b5d5609b", + "imageID": "sha256:ebbe9df4abf4dd9a739b33ab75d1fee2086713829a437f9d1e5e3de7b21e8d5f", + "manifestDigest": "sha256:5fe577767eba4cca2fe7594f6df94ca2b0f639a2ee8794f99f2ac49b81b5d419", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "tags": [ + "ghcr.io/tap8stry/git-init:v0.21.0" + ], + "imageSize": 81343568, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:0fcbbeeeb0d7fc5c06362d7a6717b999e605574c7210eff4f7418f6e9be9fbfe", + "size": 5610661 + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:8f1d7de99bcffd39d4461b917a5313cfa0415f33eac9412a9b6138a27121c7e6", + "size": 4686 + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:165c22a332e306497ffa210ce9f284906fe0bf6340d20c5f8521e064323ba52a", + "size": 37280295 + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:0519ec3ee06ceaaa19b5682db6a01d408f9be6d74dc0f453e416fc92b654ce2f", + "size": 9503892 + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:a79b016ff7714845775dd921c15fab70652344015e91ebff2ccec49d6792ac11", + "size": 28944034 + } + ], + "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoxNzM1LCJkaWdlc3QiOiJzaGEyNTY6ZWJiZTlkZjRhYmY0ZGQ5YTczOWIzM2FiNzVkMWZlZTIwODY3MTM4MjlhNDM3ZjlkMWU1ZTNkZTdiMjFlOGQ1ZiJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjo1ODgxMzQ0LCJkaWdlc3QiOiJzaGEyNTY6MGZjYmJlZWViMGQ3ZmM1YzA2MzYyZDdhNjcxN2I5OTllNjA1NTc0YzcyMTBlZmY0Zjc0MThmNmU5YmU5ZmJmZSJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjExNzc2LCJkaWdlc3QiOiJzaGEyNTY6OGYxZDdkZTk5YmNmZmQzOWQ0NDYxYjkxN2E1MzEzY2ZhMDQxNWYzM2VhYzk0MTJhOWI2MTM4YTI3MTIxYzdlNiJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjM3NzQ5NzYwLCJkaWdlc3QiOiJzaGEyNTY6MTY1YzIyYTMzMmUzMDY0OTdmZmEyMTBjZTlmMjg0OTA2ZmUwYmY2MzQwZDIwYzVmODUyMWUwNjQzMjNiYTUyYSJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjk2NTIyMjQsImRpZ2VzdCI6InNoYTI1NjowNTE5ZWMzZWUwNmNlYWFhMTliNTY4MmRiNmEwMWQ0MDhmOWJlNmQ3NGRjMGY0NTNlNDE2ZmM5MmI2NTRjZTJmIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6Mjg5NDY0MzIsImRpZ2VzdCI6InNoYTI1NjphNzliMDE2ZmY3NzE0ODQ1Nzc1ZGQ5MjFjMTVmYWI3MDY1MjM0NDAxNWU5MWViZmYyY2NlYzQ5ZDY3OTJhYzExIn1dfQ==", + "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImF1dGhvciI6ImdpdGh1Yi5jb20vZ29vZ2xlL2tvIiwiY3JlYXRlZCI6IjIwMjEtMDItMTZUMTk6MzU6NDNaIiwiaGlzdG9yeSI6W3siY3JlYXRlZCI6IjIwMjAtMTItMTdUMDA6MTk6NDkuMTEyNDQ1OTY1WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBBREQgZmlsZTo4ZWQ4MDAxMGU0NDNkYTE5ZDcyNTQ2YmNlZTlhMzVlMGE4ZDI0NGM3MjA1MmIxOTk0NjEwYmY1OTM5ZDQ3OWMyIGluIC8gIn0seyJjcmVhdGVkIjoiMjAyMC0xMi0xN1QwMDoxOTo0OS4yODQyMTExNDhaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApICBDTUQgW1wiL2Jpbi9zaFwiXSIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIxLTAyLTE2VDE5OjM1OjI4LjkzODk1MjU1M1oiLCJjcmVhdGVkX2J5IjoiUlVOIC9iaW4vc2ggLWMgYWRkZ3JvdXAgLVMgLWcgNjU1MzIgbm9ucm9vdCBcdTAwMjZcdTAwMjYgYWRkdXNlciAtUyAtdSA2NTUzMiBub25yb290IC1HIG5vbnJvb3QgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMS0wMi0xNlQxOTozNTozMi41NTU3NTk2NTRaIiwiY3JlYXRlZF9ieSI6IlJVTiAvYmluL3NoIC1jIGFwayBhZGQgLS11cGRhdGUgZ2l0IGdpdC1sZnMgb3BlbnNzaC1jbGllbnQgICAgIFx1MDAyNlx1MDAyNiBhcGsgdXBkYXRlICAgICBcdTAwMjZcdTAwMjYgYXBrIHVwZ3JhZGUgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJhdXRob3IiOiJrbyIsImNyZWF0ZWQiOiIwMDAxLTAxLTAxVDAwOjAwOjAwWiIsImNyZWF0ZWRfYnkiOiJrbyBwdWJsaXNoIGtvOi8vZ2l0aHViLmNvbS90ZWt0b25jZC9waXBlbGluZS9jbWQvZ2l0LWluaXQiLCJjb21tZW50Ijoia29kYXRhIGNvbnRlbnRzLCBhdCAkS09fREFUQV9QQVRIIn0seyJhdXRob3IiOiJrbyIsImNyZWF0ZWQiOiIwMDAxLTAxLTAxVDAwOjAwOjAwWiIsImNyZWF0ZWRfYnkiOiJrbyBwdWJsaXNoIGtvOi8vZ2l0aHViLmNvbS90ZWt0b25jZC9waXBlbGluZS9jbWQvZ2l0LWluaXQiLCJjb21tZW50IjoiZ28gYnVpbGQgb3V0cHV0LCBhdCAva28tYXBwL2dpdC1pbml0In1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6MGZjYmJlZWViMGQ3ZmM1YzA2MzYyZDdhNjcxN2I5OTllNjA1NTc0YzcyMTBlZmY0Zjc0MThmNmU5YmU5ZmJmZSIsInNoYTI1Njo4ZjFkN2RlOTliY2ZmZDM5ZDQ0NjFiOTE3YTUzMTNjZmEwNDE1ZjMzZWFjOTQxMmE5YjYxMzhhMjcxMjFjN2U2Iiwic2hhMjU2OjE2NWMyMmEzMzJlMzA2NDk3ZmZhMjEwY2U5ZjI4NDkwNmZlMGJmNjM0MGQyMGM1Zjg1MjFlMDY0MzIzYmE1MmEiLCJzaGEyNTY6MDUxOWVjM2VlMDZjZWFhYTE5YjU2ODJkYjZhMDFkNDA4ZjliZTZkNzRkYzBmNDUzZTQxNmZjOTJiNjU0Y2UyZiIsInNoYTI1NjphNzliMDE2ZmY3NzE0ODQ1Nzc1ZGQ5MjFjMTVmYWI3MDY1MjM0NDAxNWU5MWViZmYyY2NlYzQ5ZDY3OTJhYzExIl19LCJjb25maWciOnsiQ21kIjpbIi9iaW4vc2giXSwiRW50cnlwb2ludCI6WyIva28tYXBwL2dpdC1pbml0Il0sIkVudiI6WyJQQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2Jpbjova28tYXBwIiwiS09fREFUQV9QQVRIPS92YXIvcnVuL2tvIl19fQ==", + "repoDigests": [ + "ghcr.io/tap8stry/git-init@sha256:322e3502c1e6fba5f1869efb55cfd998a3679e073840d33eb0e3c482b5d5609b" + ] + } + }, + "distro": { + "name": "Alpine Linux", + "version": "", + "idLike": null + }, + "descriptor": { + "name": "grype", + "version": "0.32.0", + "configuration": { + "configPath": "", + "output": "json", + "file": "", + "output-template-file": "", + "quiet": false, + "check-for-app-update": true, + "only-fixed": false, + "search": { + "scope": "Squashed", + "unindexed-archives": false, + "indexed-archives": true + }, + "ignore": null, + "exclude": [], + "db": { + "cache-dir": "/home/jim/.cache/grype/db", + "update-url": "https://toolbox-data.anchore.io/grype/databases/listing.json", + "ca-cert": "", + "auto-update": true, + "validate-by-hash-on-start": false + }, + "dev": { + "profile-cpu": false, + "profile-mem": false + }, + "fail-on-severity": "", + "registry": { + "insecure-skip-tls-verify": false, + "insecure-use-http": false, + "auth": [] + }, + "log": { + "structured": false, + "level": "", + "file": "" + } + }, + "db": { + "built": "2022-05-15T08:15:19Z", + "schemaVersion": 3, + "location": "/home/jim/.cache/grype/db/3", + "checksum": "sha256:4e6836ac8db4fbe1488c6a81f37cdc044e3d22041573c7a552aa0e053efa5c29", + "error": null + } + } + } +} +` + +func Test_Conditions(t *testing.T) { + + conditions := []v1.AnyAllConditions{ + { + AnyConditions: []v1.Condition{ + { + RawKey: &apiextv1.JSON{Raw: []byte("\"{{ matches[].vulnerability[].cvss[?metrics.impactScore > '8.0'][] | length(@) }}\"")}, + Operator: "Equals", + RawValue: &apiextv1.JSON{Raw: []byte("\"0\"")}, + }, + { + RawKey: &apiextv1.JSON{Raw: []byte("\"{{ source.target.userInput }}\"")}, + Operator: "Equals", + RawValue: &apiextv1.JSON{Raw: []byte("\"ghcr.io/tap8stry/git-init:v0.21.0@sha256:322e3502c1e6fba5f1869efb55cfd998a3679e073840d33eb0e3c482b5d5609b\"")}, + }, + }, + }, + } + + ctx := context.NewContext() + img := api.ImageInfo{Pointer: "/spec/containers/0/image"} + img.ImageInfo = image.ImageInfo{ + Registry: "docker.io", + Name: "nginx", + Path: "test/nginx", + Tag: "latest", + } + + var dataMap map[string]interface{} + err := json.Unmarshal([]byte(scanPredicate), &dataMap) + assert.NilError(t, err) + + pass, err := evaluateConditions(conditions, ctx, dataMap, img, log.Log) + assert.NilError(t, err) + assert.Equal(t, pass, true) +} diff --git a/pkg/engine/imageVerify.go b/pkg/engine/imageVerify.go index 855cae263a..c514d6c0f6 100644 --- a/pkg/engine/imageVerify.go +++ b/pkg/engine/imageVerify.go @@ -163,17 +163,14 @@ func (iv *imageVerifier) verify(imageVerify kyvernov1.ImageVerification, images continue } - var ruleResp *response.RuleResponse - var digest string - - if len(imageVerify.Attestors) > 0 { - if len(imageVerify.Attestations) > 0 { - ruleResp = iv.verifyAttestations(imageVerify, imageInfo) - } else { - ruleResp, digest = iv.verifySignatures(imageVerify, imageInfo) - } + verified, err := isImageVerified(iv.policyContext.NewResource, image, iv.logger) + if err == nil && verified { + iv.logger.Info("image was previously verified, skipping check", "image", image) + continue } + ruleResp, digest := iv.verifyImage(imageVerify, imageInfo) + if imageVerify.MutateDigest { patch, retrievedDigest, err := iv.handleMutateDigest(digest, imageInfo) if err != nil { @@ -261,15 +258,20 @@ func imageMatches(image string, imagePatterns []string) bool { return false } -func (iv *imageVerifier) verifySignatures(imageVerify kyvernov1.ImageVerification, imageInfo apiutils.ImageInfo) (*response.RuleResponse, string) { - image := imageInfo.String() - iv.logger.V(2).Info("verifying image signatures", "image", image, "attestors", len(imageVerify.Attestors), "attestations", len(imageVerify.Attestations)) +func (iv *imageVerifier) verifyImage(imageVerify kyvernov1.ImageVerification, imageInfo apiutils.ImageInfo) (*response.RuleResponse, string) { + if len(imageVerify.Attestors) <= 0 { + return nil, "" + } - var digest string + image := imageInfo.String() + iv.logger.V(2).Info("verifying image signatures", "image", image, + "attestors", len(imageVerify.Attestors), "attestations", len(imageVerify.Attestations)) + + var cosignResponse *cosign.Response for i, attestorSet := range imageVerify.Attestors { var err error path := fmt.Sprintf(".attestors[%d]", i) - digest, err = iv.verifyAttestorSet(attestorSet, imageVerify, image, path) + cosignResponse, err = iv.verifyAttestorSet(attestorSet, imageVerify, imageInfo, path) if err != nil { iv.logger.Error(err, "failed to verify signature") msg := fmt.Sprintf("failed to verify signature for %s: %s", image, err.Error()) @@ -277,19 +279,26 @@ func (iv *imageVerifier) verifySignatures(imageVerify kyvernov1.ImageVerificatio } } + if cosignResponse == nil { + return ruleError(iv.rule, response.ImageVerify, "invalid response", fmt.Errorf("nil")), "" + } + msg := fmt.Sprintf("verified image signatures for %s", image) - return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusPass, nil), digest + return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusPass, nil), cosignResponse.Digest } -func (iv *imageVerifier) verifyAttestorSet(attestorSet kyvernov1.AttestorSet, imageVerify kyvernov1.ImageVerification, image, path string) (string, error) { +func (iv *imageVerifier) verifyAttestorSet(attestorSet kyvernov1.AttestorSet, imageVerify kyvernov1.ImageVerification, + imageInfo apiutils.ImageInfo, path string) (*cosign.Response, error) { + var errorList []error verifiedCount := 0 attestorSet = expandStaticKeys(attestorSet) requiredCount := getRequiredCount(attestorSet) + image := imageInfo.String() for i, a := range attestorSet.Entries { - var digest string var entryError error + var cosignResp *cosign.Response attestorPath := fmt.Sprintf("%s.entries[%d]", path, i) if a.Attestor != nil { @@ -298,11 +307,15 @@ func (iv *imageVerifier) verifyAttestorSet(attestorSet kyvernov1.AttestorSet, im entryError = errors.Wrapf(err, "failed to unmarshal nested attestor %s", attestorPath) } else { attestorPath += ".attestor" - digest, entryError = iv.verifyAttestorSet(*nestedAttestorSet, imageVerify, image, attestorPath) + cosignResp, entryError = iv.verifyAttestorSet(*nestedAttestorSet, imageVerify, imageInfo, attestorPath) } } else { opts, subPath := iv.buildOptionsAndPath(a, imageVerify, image) - digest, entryError = cosign.VerifySignature(*opts) + cosignResp, entryError = cosign.Verify(*opts) + if opts.FetchAttestations && entryError == nil { + entryError = iv.verifyAttestations(cosignResp.Statements, imageVerify, imageInfo) + } + if entryError != nil { entryError = fmt.Errorf("%s: %s", attestorPath+subPath, entryError.Error()) } @@ -312,7 +325,7 @@ func (iv *imageVerifier) verifyAttestorSet(attestorSet kyvernov1.AttestorSet, im verifiedCount++ if verifiedCount >= requiredCount { iv.logger.V(2).Info("image verification succeeded", "verifiedCount", verifiedCount, "requiredCount", requiredCount) - return digest, nil + return cosignResp, nil } } else { errorList = append(errorList, entryError) @@ -321,7 +334,7 @@ func (iv *imageVerifier) verifyAttestorSet(attestorSet kyvernov1.AttestorSet, im iv.logger.Info("image verification failed", "verifiedCount", verifiedCount, "requiredCount", requiredCount, "errors", errorList) err := engineUtils.CombineErrors(errorList) - return "", err + return nil, err } func expandStaticKeys(attestorSet kyvernov1.AttestorSet) kyvernov1.AttestorSet { @@ -388,6 +401,10 @@ func (iv *imageVerifier) buildOptionsAndPath(attestor kyvernov1.Attestor, imageV opts.Roots = imageVerify.Roots } + if len(imageVerify.Attestations) > 0 { + opts.FetchAttestations = true + } + if attestor.Keys != nil { path = path + ".keys" opts.Key = attestor.Keys.PublicKeys @@ -432,46 +449,39 @@ func makeAddDigestPatch(imageInfo apiutils.ImageInfo, digest string) ([]byte, er return json.Marshal(patch) } -func (iv *imageVerifier) verifyAttestations(imageVerify kyvernov1.ImageVerification, imageInfo apiutils.ImageInfo) *response.RuleResponse { +func (iv *imageVerifier) verifyAttestations(statements []map[string]interface{}, imageVerify kyvernov1.ImageVerification, imageInfo apiutils.ImageInfo) error { image := imageInfo.String() - start := time.Now() - - statements, err := cosign.FetchAttestations(image, imageVerify) - if err != nil { - iv.logger.Info("failed to fetch attestations", "image", image, "error", err, "duration", time.Since(start).Seconds()) - return ruleError(iv.rule, response.ImageVerify, fmt.Sprintf("failed to fetch attestations for %s", image), err) - } - - iv.logger.V(4).Info("received attestations", "count", len(statements)) - statementsByPredicate := buildStatementMap(statements) + statementsByPredicate, types := buildStatementMap(statements) + iv.logger.V(4).Info("checking attestations", "predicates", types, "image", image) for _, ac := range imageVerify.Attestations { statements := statementsByPredicate[ac.PredicateType] if statements == nil { - msg := fmt.Sprintf("predicate type %s not found", ac.PredicateType) - return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusFail, nil) + iv.logger.Info("attestation predicate type %s not found", "predicates", types, "image", imageInfo.String()) + return fmt.Errorf("predicate type %s not found", ac.PredicateType) } + iv.logger.Info("checking attestation predicate type %s", "predicates", types, "image", imageInfo.String()) + for _, s := range statements { val, err := iv.checkAttestations(ac, s, imageInfo) if err != nil { - return ruleError(iv.rule, response.ImageVerify, "failed to check attestation", err) + return errors.Wrap(err, "failed to check attestations") } if !val { - msg := fmt.Sprintf("attestation checks failed for %s and predicate %s", imageInfo.String(), ac.PredicateType) - return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusFail, nil) + return fmt.Errorf("attestation checks failed for %s and predicate %s", imageInfo.String(), ac.PredicateType) } } } - msg := fmt.Sprintf("attestation checks passed for %s", imageInfo.String()) - iv.logger.V(2).Info(msg) - return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusPass, nil) + iv.logger.V(3).Info("attestation checks passed for %s", imageInfo.String()) + return nil } -func buildStatementMap(statements []map[string]interface{}) map[string][]map[string]interface{} { +func buildStatementMap(statements []map[string]interface{}) (map[string][]map[string]interface{}, []string) { results := map[string][]map[string]interface{}{} + var predicateTypes []string for _, s := range statements { predicateType := s["predicateType"].(string) if results[predicateType] != nil { @@ -479,9 +489,11 @@ func buildStatementMap(statements []map[string]interface{}) map[string][]map[str } else { results[predicateType] = []map[string]interface{}{s} } + + predicateTypes = append(predicateTypes, predicateType) } - return results + return results, predicateTypes } func (iv *imageVerifier) checkAttestations(a kyvernov1.Attestation, s map[string]interface{}, img apiutils.ImageInfo) (bool, error) { @@ -492,24 +504,34 @@ func (iv *imageVerifier) checkAttestations(a kyvernov1.Attestation, s map[string iv.policyContext.JSONContext.Checkpoint() defer iv.policyContext.JSONContext.Restore() + return evaluateConditions(a.Conditions, iv.policyContext.JSONContext, s, img, iv.logger) +} + +func evaluateConditions( + conditions []kyvernov1.AnyAllConditions, + ctx context.Interface, + s map[string]interface{}, + img apiutils.ImageInfo, + log logr.Logger) (bool, error) { + predicate, ok := s["predicate"].(map[string]interface{}) if !ok { return false, fmt.Errorf("failed to extract predicate from statement: %v", s) } - if err := context.AddJSONObject(iv.policyContext.JSONContext, predicate); err != nil { + if err := context.AddJSONObject(ctx, predicate); err != nil { return false, errors.Wrapf(err, fmt.Sprintf("failed to add Statement to the context %v", s)) } - if err := iv.policyContext.JSONContext.AddImageInfo(img); err != nil { + if err := ctx.AddImageInfo(img); err != nil { return false, errors.Wrapf(err, fmt.Sprintf("failed to add image to the context %v", s)) } - conditions, err := variables.SubstituteAllInConditions(iv.logger, iv.policyContext.JSONContext, a.Conditions) + c, err := variables.SubstituteAllInConditions(log, ctx, conditions) if err != nil { return false, errors.Wrapf(err, "failed to substitute variables in attestation conditions") } - pass := variables.EvaluateAnyAllConditions(iv.logger, iv.policyContext.JSONContext, conditions) + pass := variables.EvaluateAnyAllConditions(log, ctx, c) return pass, nil } diff --git a/pkg/engine/variables/evaluate.go b/pkg/engine/variables/evaluate.go index 6797f7b0fd..0ff27d8f2d 100644 --- a/pkg/engine/variables/evaluate.go +++ b/pkg/engine/variables/evaluate.go @@ -52,12 +52,15 @@ func evaluateAnyAllConditions(log logr.Logger, ctx context.EvalInterface, condit break } } + + log.Info("no condition passed for 'any' block", "any", anyConditions) } // update the allConditionsResult if they are present for _, condition := range allConditions { if !Evaluate(log, ctx, condition) { allConditionsResult = false + log.Info("a condition failed in 'all' block", "condition", condition) break } }