From 26e03163a8447082829517a2a25a25b95f8273a1 Mon Sep 17 00:00:00 2001 From: "emilyye@google.com" Date: Tue, 26 May 2020 09:34:40 -0700 Subject: [PATCH] add occurrence --- products/containeranalysis/api.yaml | 113 ++++++- products/containeranalysis/terraform.yaml | 25 +- .../containeranalysis_occurrence.go.erb | 43 +++ .../containeranalysis_occurrence.go.erb | 43 +++ ...iner_analysis_occurence_attestation.tf.erb | 35 +++ .../container_analysis_occurrence_kms.tf.erb | 51 ++++ .../containeranalysis_occurrence.go.erb | 23 ++ ...urce_container_analysis_occurrence_test.go | 277 ++++++++++++++++++ .../terraform/utils/bootstrap_utils_test.go | 98 +++---- .../binauthz/generated_payload.json.tmpl | 12 + 10 files changed, 656 insertions(+), 64 deletions(-) create mode 100644 templates/terraform/decoders/containeranalysis_occurrence.go.erb create mode 100644 templates/terraform/encoders/containeranalysis_occurrence.go.erb create mode 100644 templates/terraform/examples/container_analysis_occurence_attestation.tf.erb create mode 100644 templates/terraform/examples/container_analysis_occurrence_kms.tf.erb create mode 100644 templates/terraform/update_encoder/containeranalysis_occurrence.go.erb create mode 100644 third_party/terraform/tests/resource_container_analysis_occurrence_test.go create mode 100644 third_party/terraform/utils/test-fixtures/binauthz/generated_payload.json.tmpl diff --git a/products/containeranalysis/api.yaml b/products/containeranalysis/api.yaml index 276e1fe0381d..68ab4527f04b 100644 --- a/products/containeranalysis/api.yaml +++ b/products/containeranalysis/api.yaml @@ -31,11 +31,12 @@ objects: update_verb: :PATCH update_mask: true description: | - A Container Analysis note is a high-level piece of metadata that + A Container Analysis note is a high-level piece of metadata that describes a type of analysis that can be done for a resource. references: !ruby/object:Api::Resource::ReferenceLinks guides: 'Official Documentation': 'https://cloud.google.com/container-analysis/' + 'Creating Attestations (Occurrences)': 'https://cloud.google.com/binary-authorization/docs/making-attestations' api: 'https://cloud.google.com/container-analysis/api/reference/rest/' properties: - !ruby/object:Api::Type::String @@ -132,3 +133,113 @@ objects: The human readable name of this Attestation Authority, for example "qa". required: true + + - !ruby/object:Api::Resource + name: 'Occurrence' + base_url: projects/{{project}}/occurrences + self_link: projects/{{project}}/occurrences/{{name}} + update_verb: :PATCH + update_mask: true + description: | + An occurrence is an instance of a Note, or type of analysis that + can be done for a resource. + references: !ruby/object:Api::Resource::ReferenceLinks + guides: + 'Official Documentation': 'https://cloud.google.com/container-analysis/' + api: 'https://cloud.google.com/container-analysis/api/reference/rest/' + properties: + - !ruby/object:Api::Type::String + name: name + description: | + The name of the occurrence. + output: true + - !ruby/object:Api::Type::String + name: resourceUri + description: | + Required. Immutable. A URI that represents the resource for which + the occurrence applies. For example, + https://gcr.io/project/image@sha256:123abc for a Docker image. + required: true + input: true + - !ruby/object:Api::Type::String + name: noteName + description: | + The analysis note associated with this occurrence, in the form of + projects/[PROVIDER_ID]/notes/[NOTE_ID]. This field can be used as a + filter in list requests. + required: true + input: true + - !ruby/object:Api::Type::String + name: kind + description: | + The note kind which explicitly denotes which of the occurrence + details are specified. This field can be used as a filter in list + requests. + output: true + - !ruby/object:Api::Type::String + name: remediation + description: | + A description of actions that can be taken to remedy the note. + - !ruby/object:Api::Type::Time + name: createTime + description: The time when the repository was created. + output: true + - !ruby/object:Api::Type::Time + name: updateTime + description: The time when the repository was last updated. + output: true + - !ruby/object:Api::Type::NestedObject + name: attestation + description: | + Occurrence that represents a single "attestation". The authenticity + of an attestation can be verified using the attached signature. + If the verifier trusts the public key of the signer, then verifying + the signature is sufficient to establish trust. In this circumstance, + the authority to which this attestation is attached is primarily + useful for lookup (how to find this attestation if you already + know the authority and artifact to be verified) and intent (for + which authority this attestation was intended to sign. + required: true + properties: + - !ruby/object:Api::Type::String + name: serializedPayload + description: | + The serialized payload that is verified by one or + more signatures. A base64-encoded string. + required: true + - !ruby/object:Api::Type::Array + name: signatures + description: | + One or more signatures over serializedPayload. + Verifier implementations should consider this attestation + message verified if at least one signature verifies + serializedPayload. See Signature in common.proto for more + details on signature structure and verification. + required: true + item_type: !ruby/object:Api::Type::NestedObject + properties: + - !ruby/object:Api::Type::String + name: signature + description: | + The content of the signature, an opaque bytestring. + The payload that this signature verifies MUST be + unambiguously provided with the Signature during + verification. A wrapper message might provide the + payload explicitly. Alternatively, a message might + have a canonical serialization that can always be + unambiguously computed to derive the payload. + - !ruby/object:Api::Type::String + name: publicKeyId + required: true + description: | + The identifier for the public key that verifies this + signature. The publicKeyId MUST be an RFC3986 conformant + URI. * When possible, the publicKeyId SHOULD be an + immutable reference, such as a cryptographic digest. + Examples of valid publicKeyIds: + + * OpenPGP V4 public key fingerprint. See https://www.iana.org/assignments/uri-schemes/prov/openpgp4fpr + for more details on this scheme. + * `openpgp4fpr:74FAF3B861BDA0870C7B6DEF607E48D2A663AEEA` + * RFC6920 digest-named SubjectPublicKeyInfo (digest of the DER serialization): + * "ni:///sha-256;cD9o9Cq6LG3jD0iKXqEi_vdjJGecm_iXkbqVoScViaU" diff --git a/products/containeranalysis/terraform.yaml b/products/containeranalysis/terraform.yaml index 548ec036cca2..3a87f106bd92 100644 --- a/products/containeranalysis/terraform.yaml +++ b/products/containeranalysis/terraform.yaml @@ -14,10 +14,10 @@ --- !ruby/object:Provider::Terraform::Config overrides: !ruby/object:Overrides::ResourceOverrides Note: !ruby/object:Overrides::Terraform::ResourceOverride + mutex: "projects/{{project}}/notes/{{name}}" id_format: "projects/{{project}}/notes/{{name}}" import_format: ["projects/{{project}}/notes/{{name}}"] custom_code: !ruby/object:Provider::Terraform::CustomCode - # pre_update: 'templates/terraform/pre_update/containeranalysis_note.erb' encoder: templates/terraform/encoders/containeranalysis_attestation_field_name.go.erb decoder: templates/terraform/decoders/containeranalysis_attestation_field_name.go.erb examples: @@ -38,6 +38,29 @@ overrides: !ruby/object:Overrides::ResourceOverrides is_set: true relatedNoteNames: !ruby/object:Overrides::Terraform::PropertyOverride is_set: true + Occurrence: !ruby/object:Overrides::Terraform::ResourceOverride + # "projects/{{project}}/notes/{{name}}" + mutex: "{{note_name}}" + id_format: "projects/{{project}}/occurrences/{{name}}" + import_format: ["projects/{{project}}/occurrences/{{name}}"] + examples: + - !ruby/object:Provider::Terraform::Examples + name: "container_analysis_occurrence_kms" + # Occurrence requires custom logic for signing payloads. + skip_test: true + primary_resource_id: "occurrence" + vars: + note_name: "attestation-note" + attestor: "attestor" + custom_code: !ruby/object:Provider::Terraform::CustomCode + encoder: templates/terraform/encoders/containeranalysis_occurrence.go.erb + update_encoder: templates/terraform/update_encoder/containeranalysis_occurrence.go.erb + decoder: templates/terraform/decoders/containeranalysis_occurrence.go.erb + properties: + name: !ruby/object:Overrides::Terraform::PropertyOverride + custom_flatten: templates/terraform/custom_flatten/name_from_self_link.erb + attestation.signatures: !ruby/object:Overrides::Terraform::PropertyOverride + is_set: true # This is for copying files over files: !ruby/object:Provider::Config::Files diff --git a/templates/terraform/decoders/containeranalysis_occurrence.go.erb b/templates/terraform/decoders/containeranalysis_occurrence.go.erb new file mode 100644 index 000000000000..0b7dbc5c91bc --- /dev/null +++ b/templates/terraform/decoders/containeranalysis_occurrence.go.erb @@ -0,0 +1,43 @@ +<%# The license inside this block applies to this file. + # Copyright 2020 Google Inc. + # 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. +-%> +<% unless version == 'ga' -%> +// Resource object was flattened in GA API +if nestedResource, ok := res["resource"]; ok { + if resObj, ok := nestedResource.(map[string]interface{}); ok { + res["resourceUri"] = resObj["uri"] + delete(res, "resource") + } +} + +// Beta attestation.attestation.genericSignedAttestation +// => GA attestation +if attV, ok := res["attestation"]; ok && attV != nil { + att := attV.(map[string]interface{}) + if nestedAttV, ok := att["attestation"]; ok && nestedAttV != nil { + nestedAtt := nestedAttV.(map[string]interface{}) + if genericV, ok := nestedAtt["genericSignedAttestation"]; ok { + genericAtt := genericV.(map[string]interface{}) + res["attestation"] = map[string]interface{}{ + "serializedPayload": genericAtt["serializedPayload"], + "signatures": genericAtt["signatures"], + } + } + } +} + +<% else -%> +// encoder logic only in non-GA version +<% end -%> +return res, nil diff --git a/templates/terraform/encoders/containeranalysis_occurrence.go.erb b/templates/terraform/encoders/containeranalysis_occurrence.go.erb new file mode 100644 index 000000000000..5697d9c8fa66 --- /dev/null +++ b/templates/terraform/encoders/containeranalysis_occurrence.go.erb @@ -0,0 +1,43 @@ +<%# The license inside this block applies to this file. + # Copyright 2020 Google Inc. + # 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. +-%> +<% unless version == 'ga' -%> +// Resource object was flattened in GA API +if resourceuri, ok := obj["resourceUri"]; ok { + obj["resource"] = map[string]interface{}{ + "uri": resourceuri, + } + delete(obj, "resourceUri") +} + + +// Beta `attestation.genericSignedAttestation` was flattened to just +// `attestation` (no contentType) in GA +if v, ok := obj["attestation"]; ok && v != nil { + gaAtt := v.(map[string]interface{}) + obj["attestation"] = map[string]interface{}{ + "attestation": map[string]interface{}{ + "genericSignedAttestation": map[string]interface{}{ + "contentType": "SIMPLE_SIGNING_JSON", + "serializedPayload": gaAtt["serializedPayload"], + "signatures": gaAtt["signatures"], + }, + }, + } +} +<% else -%> +// encoder logic only in non-GA versions +<% end -%> + +return obj, nil diff --git a/templates/terraform/examples/container_analysis_occurence_attestation.tf.erb b/templates/terraform/examples/container_analysis_occurence_attestation.tf.erb new file mode 100644 index 000000000000..8d0fceec2b8d --- /dev/null +++ b/templates/terraform/examples/container_analysis_occurence_attestation.tf.erb @@ -0,0 +1,35 @@ +resource "google_binary_authorization_attestor" "<%= ctx[:primary_resource_id] %>" { + name = "<%= ctx[:vars]["attestor_name"] %>" + attestation_authority_note { + note_reference = google_container_analysis_note.note.name + public_keys { + ascii_armored_pgp_public_key = <" + attestation_authority { + hint { + human_readable_name = "Attestor Note" + } + } +} diff --git a/templates/terraform/examples/container_analysis_occurrence_kms.tf.erb b/templates/terraform/examples/container_analysis_occurrence_kms.tf.erb new file mode 100644 index 000000000000..505393076c54 --- /dev/null +++ b/templates/terraform/examples/container_analysis_occurrence_kms.tf.erb @@ -0,0 +1,51 @@ +resource "google_binary_authorization_attestor" "attestor" { + name = "<%= ctx[:vars]["attestor"] %>" + attestation_authority_note { + note_reference = google_container_analysis_note.note.name + public_keys { + id = data.google_kms_crypto_key_version.version.id + pkix_public_key { + public_key_pem = data.google_kms_crypto_key_version.version.public_key[0].pem + signature_algorithm = data.google_kms_crypto_key_version.version.public_key[0].algorithm + } + } + } +} + +resource "google_container_analysis_note" "note" { + name = "<%= ctx[:vars]["note_name"] %>" + attestation_authority { + hint { + human_readable_name = "Attestor Note" + } + } +} + +data "google_kms_key_ring" "keyring" { + name = "my-key-ring" + location = "global" +} + +data "google_kms_crypto_key" "crypto-key" { + name = "my-key" + key_ring = data.google_kms_key_ring.keyring.self_link +} + +data "google_kms_crypto_key_version" "version" { + crypto_key = data.google_kms_crypto_key.crypto-key.self_link +} + +resource "google_container_analysis_occurrence" "<%= ctx[:primary_resource_id] %>" { + resource_uri = "gcr.io/my-project/my-image" + note_name = google_container_analysis_note.note.id + + // See "Creating Attestations" Guide for expected + // payload and signature formats. + attestation { + serialized_payload = filebase64("path/to/my/payload.json") + signatures { + public_key_id = data.google_kms_crypto_key_version.version.id + serialized_payload = filebase64("path/to/my/payload.json.sig") + } + } +} diff --git a/templates/terraform/update_encoder/containeranalysis_occurrence.go.erb b/templates/terraform/update_encoder/containeranalysis_occurrence.go.erb new file mode 100644 index 000000000000..4ce716cf1483 --- /dev/null +++ b/templates/terraform/update_encoder/containeranalysis_occurrence.go.erb @@ -0,0 +1,23 @@ +<%# The license inside this block applies to this file. + # Copyright 2020 Google Inc. + # 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. +-%> +// Note is required, even for PATCH +noteNameProp, err := expandContainerAnalysisOccurrenceNoteName(d.Get("note_name"), d, meta.(*Config)) +if err != nil { + return nil, err +} else if v, ok := d.GetOkExists("note_name"); !isEmptyValue(reflect.ValueOf(noteNameProp)) && (ok || !reflect.DeepEqual(v, noteNameProp)) { + obj["noteName"] = noteNameProp +} + +return resource<%= resource_name -%>Encoder(d, meta, obj) diff --git a/third_party/terraform/tests/resource_container_analysis_occurrence_test.go b/third_party/terraform/tests/resource_container_analysis_occurrence_test.go new file mode 100644 index 000000000000..f095d0c24285 --- /dev/null +++ b/third_party/terraform/tests/resource_container_analysis_occurrence_test.go @@ -0,0 +1,277 @@ +package google + +import ( + "encoding/base64" + "fmt" + "io/ioutil" + "testing" + + "crypto/sha512" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "google.golang.org/api/cloudkms/v1" +) + +const testAttestationOccurrenceImageUrl = "gcr.io/cloud-marketplace/google/ubuntu1804" +const testAttestationOccurrenceImageDigest = "sha256:3593cd4ac7d782d460dc86ba9870a3beaf81c8f5cdbcc8880bf9a5ef6af10c5a" +const testAttestationOccurrencePayloadTemplate = "test-fixtures/binauthz/generated_payload.json.tmpl" + +var testAttestationOccurrenceFullImagePath = fmt.Sprintf("%s@%s", testAttestationOccurrenceImageUrl, testAttestationOccurrenceImageDigest) + +func getTestOccurrenceAttestationPayload(t *testing.T) string { + payloadTmpl, err := ioutil.ReadFile(testAttestationOccurrencePayloadTemplate) + if err != nil { + t.Fatal(err.Error()) + } + return fmt.Sprintf(string(payloadTmpl), + testAttestationOccurrenceImageUrl, + testAttestationOccurrenceImageDigest) +} + +func getSignedTestOccurrenceAttestationPayload( + t *testing.T, config *Config, + signingKey bootstrappedKMS, rawPayload string) string { + pbytes := []byte(rawPayload) + ssum := sha512.Sum512(pbytes) + hashed := base64.StdEncoding.EncodeToString(ssum[:]) + signed, err := config.clientKms.Projects.Locations.KeyRings.CryptoKeys. + CryptoKeyVersions.AsymmetricSign( + fmt.Sprintf("%s/cryptoKeyVersions/1", signingKey.CryptoKey.Name), + &cloudkms.AsymmetricSignRequest{ + Digest: &cloudkms.Digest{ + Sha512: hashed, + }, + }).Do() + if err != nil { + t.Fatalf("Unable to sign attestation payload with KMS key: %s", err) + } + + return signed.Signature +} + +func TestAccContainerAnalysisOccurrence_basic(t *testing.T) { + t.Parallel() + randSuffix := randString(t, 10) + + config := BootstrapConfig(t) + if config == nil { + return + } + + signKey := BootstrapKMSKeyWithPurpose(t, "ASYMMETRIC_SIGN") + payload := getTestOccurrenceAttestationPayload(t) + signed := getSignedTestOccurrenceAttestationPayload(t, config, signKey, payload) + params := map[string]interface{}{ + "random_suffix": randSuffix, + "image_url": testAttestationOccurrenceFullImagePath, + "key_ring": GetResourceNameFromSelfLink(signKey.KeyRing.Name), + "crypto_key": GetResourceNameFromSelfLink(signKey.CryptoKey.Name), + "payload": base64.StdEncoding.EncodeToString([]byte(payload)), + "signature": base64.StdEncoding.EncodeToString([]byte(signed)), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckContainerAnalysisNoteDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerAnalysisOccurence_basic(params), + }, + { + ResourceName: "google_container_analysis_occurrence.occurrence", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccContainerAnalysisOccurrence_multipleSignatures(t *testing.T) { + t.Parallel() + randSuffix := randString(t, 10) + + config := BootstrapConfig(t) + if config == nil { + return + } + + payload := getTestOccurrenceAttestationPayload(t) + key1 := BootstrapKMSKeyWithPurposeInLocationAndName(t, "ASYMMETRIC_SIGN", "global", "tf-bootstrap-binauthz-key1") + signature1 := getSignedTestOccurrenceAttestationPayload(t, config, key1, payload) + + key2 := BootstrapKMSKeyWithPurposeInLocationAndName(t, "ASYMMETRIC_SIGN", "global", "tf-bootstrap-binauthz-key2") + signature2 := getSignedTestOccurrenceAttestationPayload(t, config, key2, payload) + + paramsMultipleSignatures := map[string]interface{}{ + "random_suffix": randSuffix, + "image_url": testAttestationOccurrenceFullImagePath, + "key_ring": GetResourceNameFromSelfLink(key1.KeyRing.Name), + "payload": base64.StdEncoding.EncodeToString([]byte(payload)), + "key1": GetResourceNameFromSelfLink(key1.CryptoKey.Name), + "signature1": base64.StdEncoding.EncodeToString([]byte(signature1)), + "key2": GetResourceNameFromSelfLink(key2.CryptoKey.Name), + "signature2": base64.StdEncoding.EncodeToString([]byte(signature2)), + } + paramsSingle := map[string]interface{}{ + "random_suffix": randSuffix, + "image_url": testAttestationOccurrenceFullImagePath, + "key_ring": GetResourceNameFromSelfLink(key1.KeyRing.Name), + "crypto_key": GetResourceNameFromSelfLink(key1.CryptoKey.Name), + "payload": base64.StdEncoding.EncodeToString([]byte(payload)), + "signature": base64.StdEncoding.EncodeToString([]byte(signature1)), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckContainerAnalysisNoteDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerAnalysisOccurence_multipleSignatures(paramsMultipleSignatures), + }, + { + ResourceName: "google_container_analysis_occurrence.occurrence", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccContainerAnalysisOccurence_basic(paramsSingle), + }, + { + ResourceName: "google_container_analysis_occurrence.occurrence", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccContainerAnalysisOccurence_basic(params map[string]interface{}) string { + return Nprintf(` +resource "google_binary_authorization_attestor" "attestor" { + name = "test-attestor%{random_suffix}" + attestation_authority_note { + note_reference = google_container_analysis_note.note.name + public_keys { + id = data.google_kms_crypto_key_version.version.id + pkix_public_key { + public_key_pem = data.google_kms_crypto_key_version.version.public_key[0].pem + signature_algorithm = data.google_kms_crypto_key_version.version.public_key[0].algorithm + } + } + } +} + +resource "google_container_analysis_note" "note" { + name = "test-attestor-note%{random_suffix}" + attestation_authority { + hint { + human_readable_name = "Attestor Note" + } + } +} + +data "google_kms_key_ring" "keyring" { + name = "%{key_ring}" + location = "global" +} + +data "google_kms_crypto_key" "crypto-key" { + name = "%{crypto_key}" + key_ring = data.google_kms_key_ring.keyring.self_link +} + +data "google_kms_crypto_key_version" "version" { + crypto_key = data.google_kms_crypto_key.crypto-key.self_link +} + +resource "google_container_analysis_occurrence" "occurrence" { + resource_uri = "%{image_url}" + note_name = google_container_analysis_note.note.id + + attestation { + serialized_payload = "%{payload}" + signatures { + public_key_id = data.google_kms_crypto_key_version.version.id + signature = "%{signature}" + } + } +} +`, params) +} + +func testAccContainerAnalysisOccurence_multipleSignatures(params map[string]interface{}) string { + return Nprintf(` +resource "google_binary_authorization_attestor" "attestor" { + name = "test-attestor%{random_suffix}" + attestation_authority_note { + note_reference = google_container_analysis_note.note.name + public_keys { + id = data.google_kms_crypto_key_version.version-key1.id + pkix_public_key { + public_key_pem = data.google_kms_crypto_key_version.version-key1.public_key[0].pem + signature_algorithm = data.google_kms_crypto_key_version.version-key1.public_key[0].algorithm + } + } + + public_keys { + id = data.google_kms_crypto_key_version.version-key2.id + pkix_public_key { + public_key_pem = data.google_kms_crypto_key_version.version-key2.public_key[0].pem + signature_algorithm = data.google_kms_crypto_key_version.version-key2.public_key[0].algorithm + } + } + } +} + +resource "google_container_analysis_note" "note" { + name = "test-attestor-note%{random_suffix}" + attestation_authority { + hint { + human_readable_name = "Attestor Note" + } + } +} + +data "google_kms_key_ring" "keyring" { + name = "%{key_ring}" + location = "global" +} + +data "google_kms_crypto_key" "crypto-key1" { + name = "%{key1}" + key_ring = data.google_kms_key_ring.keyring.self_link +} + +data "google_kms_crypto_key" "crypto-key2" { + name = "%{key2}" + key_ring = data.google_kms_key_ring.keyring.self_link +} + +data "google_kms_crypto_key_version" "version-key1" { + crypto_key = data.google_kms_crypto_key.crypto-key1.self_link +} + +data "google_kms_crypto_key_version" "version-key2" { + crypto_key = data.google_kms_crypto_key.crypto-key2.self_link +} + +resource "google_container_analysis_occurrence" "occurrence" { + resource_uri = "%{image_url}" + note_name = google_container_analysis_note.note.id + + attestation { + serialized_payload = "%{payload}" + signatures { + public_key_id = data.google_kms_crypto_key_version.version-key1.id + signature = "%{signature1}" + } + + signatures { + public_key_id = data.google_kms_crypto_key_version.version-key2.id + signature = "%{signature2}" + } + } +} +`, params) +} diff --git a/third_party/terraform/utils/bootstrap_utils_test.go b/third_party/terraform/utils/bootstrap_utils_test.go index 1f66cdfe23a9..267c8a1f3bce 100644 --- a/third_party/terraform/utils/bootstrap_utils_test.go +++ b/third_party/terraform/utils/bootstrap_utils_test.go @@ -51,10 +51,12 @@ func BootstrapKMSKeyWithPurpose(t *testing.T, purpose string) bootstrappedKMS { * a KMS key. **/ func BootstrapKMSKeyWithPurposeInLocation(t *testing.T, purpose, locationID string) bootstrappedKMS { - if v := os.Getenv("TF_ACC"); v == "" { - t.Skip("Acceptance tests and bootstrapping skipped unless env 'TF_ACC' set") + return BootstrapKMSKeyWithPurposeInLocationAndName(t, purpose, locationID, SharedCryptoKey[purpose]) +} - // If not running acceptance tests, return an empty object +func BootstrapKMSKeyWithPurposeInLocationAndName(t *testing.T, purpose, locationID, keyShortName string) bootstrappedKMS { + config := BootstrapConfig(t) + if config == nil { return bootstrappedKMS{ &cloudkms.KeyRing{}, &cloudkms.CryptoKey{}, @@ -65,20 +67,7 @@ func BootstrapKMSKeyWithPurposeInLocation(t *testing.T, purpose, locationID stri keyRingParent := fmt.Sprintf("projects/%s/locations/%s", projectID, locationID) keyRingName := fmt.Sprintf("%s/keyRings/%s", keyRingParent, SharedKeyRing) keyParent := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", projectID, locationID, SharedKeyRing) - keyName := fmt.Sprintf("%s/cryptoKeys/%s", keyParent, SharedCryptoKey[purpose]) - - config := &Config{ - Credentials: getTestCredsFromEnv(), - Project: getTestProjectFromEnv(), - Region: getTestRegionFromEnv(), - Zone: getTestZoneFromEnv(), - } - - ConfigureBasePaths(config) - - if err := config.LoadAndValidate(context.Background()); err != nil { - t.Errorf("Unable to bootstrap KMS key: %s", err) - } + keyName := fmt.Sprintf("%s/cryptoKeys/%s", keyParent, keyShortName) // Get or Create the hard coded shared keyring for testing kmsClient := config.clientKms @@ -118,7 +107,7 @@ func BootstrapKMSKeyWithPurposeInLocation(t *testing.T, purpose, locationID stri } cryptoKey, err = kmsClient.Projects.Locations.KeyRings.CryptoKeys.Create(keyParent, &newKey). - CryptoKeyId(SharedCryptoKey[purpose]).Do() + CryptoKeyId(keyShortName).Do() if err != nil { t.Errorf("Unable to bootstrap KMS key. Cannot create new CryptoKey: %s", err) } @@ -202,24 +191,11 @@ func impersonationServiceAccountPermissions(config *Config, sa *iam.ServiceAccou } func BootstrapServiceAccount(t *testing.T, project, testRunner string) string { - if v := os.Getenv("TF_ACC"); v == "" { - t.Skip("Acceptance tests and bootstrapping skipped unless env 'TF_ACC' set") + config := BootstrapConfig(t) + if config == nil { return "" } - config := &Config{ - Credentials: getTestCredsFromEnv(), - Project: getTestProjectFromEnv(), - Region: getTestRegionFromEnv(), - Zone: getTestZoneFromEnv(), - } - - ConfigureBasePaths(config) - - if err := config.LoadAndValidate(context.Background()); err != nil { - t.Fatalf("Bootstrapping failed. Unable to load test config: %s", err) - } - sa, err := getOrCreateServiceAccount(config, project) if err != nil { t.Fatalf("Bootstrapping failed. Cannot retrieve service account, %s", err) @@ -244,23 +220,12 @@ const SharedTestNetworkPrefix = "tf-bootstrap-net-" // testId specifies the test/suite for which a shared network is used/initialized. // Returns the name of an network, creating it if hasn't been created in the test projcet. func BootstrapSharedTestNetwork(t *testing.T, testId string) string { - if v := os.Getenv("TF_ACC"); v == "" { - t.Skip("Acceptance tests and bootstrapping skipped unless env 'TF_ACC' set") - // If not running acceptance tests, return an empty string - return "" - } - project := getTestProjectFromEnv() networkName := SharedTestNetworkPrefix + testId - config := &Config{ - Credentials: getTestCredsFromEnv(), - Project: project, - Region: getTestRegionFromEnv(), - Zone: getTestZoneFromEnv(), - } - ConfigureBasePaths(config) - if err := config.LoadAndValidate(context.Background()); err != nil { - t.Errorf("Unable to bootstrap network: %s", err) + + config := BootstrapConfig(t) + if config == nil { + return "" } log.Printf("[DEBUG] Getting shared test network %q", networkName) @@ -298,24 +263,12 @@ func BootstrapSharedTestNetwork(t *testing.T, testId string) string { var SharedServicePerimeterProjectPrefix = "tf-bootstrap-sp-" func BootstrapServicePerimeterProjects(t *testing.T, desiredProjects int) []*cloudresourcemanager.Project { - if v := os.Getenv("TF_ACC"); v == "" { - t.Skip("Acceptance tests and bootstrapping skipped unless env 'TF_ACC' set") + config := BootstrapConfig(t) + if config == nil { return nil } org := getTestOrgFromEnv(t) - config := &Config{ - Credentials: getTestCredsFromEnv(), - Project: getTestProjectFromEnv(), - Region: getTestRegionFromEnv(), - Zone: getTestZoneFromEnv(), - } - - ConfigureBasePaths(config) - - if err := config.LoadAndValidate(context.Background()); err != nil { - t.Fatalf("Bootstrapping failed. Unable to load test config: %s", err) - } // The filter endpoint works differently if you provide both the parent id and parent type, and // doesn't seem to allow for prefix matching. Don't change this to include the parent type unless @@ -361,3 +314,24 @@ func BootstrapServicePerimeterProjects(t *testing.T, desiredProjects int) []*clo return projects } + +func BootstrapConfig(t *testing.T) *Config { + if v := os.Getenv("TF_ACC"); v == "" { + t.Skip("Acceptance tests and bootstrapping skipped unless env 'TF_ACC' set") + return nil + } + + config := &Config{ + Credentials: getTestCredsFromEnv(), + Project: getTestProjectFromEnv(), + Region: getTestRegionFromEnv(), + Zone: getTestZoneFromEnv(), + } + + ConfigureBasePaths(config) + + if err := config.LoadAndValidate(context.Background()); err != nil { + t.Fatalf("Bootstrapping failed. Unable to load test config: %s", err) + } + return config +} diff --git a/third_party/terraform/utils/test-fixtures/binauthz/generated_payload.json.tmpl b/third_party/terraform/utils/test-fixtures/binauthz/generated_payload.json.tmpl new file mode 100644 index 000000000000..3db3c90fe980 --- /dev/null +++ b/third_party/terraform/utils/test-fixtures/binauthz/generated_payload.json.tmpl @@ -0,0 +1,12 @@ +{ + "critical": { + "identity": { + "docker-reference": "%s" + }, + "image": { + "%s" + }, + "type": "Google cloud binauthz container signature" + } +} +