diff --git a/0-bootstrap/main.tf b/0-bootstrap/main.tf index 3065d884c..a98cb582f 100644 --- a/0-bootstrap/main.tf +++ b/0-bootstrap/main.tf @@ -93,6 +93,7 @@ module "seed_bootstrap" { "billingbudgets.googleapis.com", "essentialcontacts.googleapis.com", "assuredworkloads.googleapis.com", + "cloudasset.googleapis.com" ] sa_org_iam_permissions = [] diff --git a/0-bootstrap/sa.tf b/0-bootstrap/sa.tf index 826a55cd6..40ba312b7 100644 --- a/0-bootstrap/sa.tf +++ b/0-bootstrap/sa.tf @@ -47,6 +47,8 @@ locals { "roles/essentialcontacts.admin", "roles/resourcemanager.tagAdmin", "roles/resourcemanager.tagUser", + "roles/cloudasset.owner", + "roles/securitycenter.sourcesEditor", ], local.common_roles)), "env" = distinct(concat([ "roles/resourcemanager.tagUser", diff --git a/1-org/envs/shared/README.md b/1-org/envs/shared/README.md index 2d09702a5..9179dee34 100644 --- a/1-org/envs/shared/README.md +++ b/1-org/envs/shared/README.md @@ -8,6 +8,7 @@ | audit\_logs\_table\_expiration\_days | Period before tables expire for all audit logs in milliseconds. Default is 30 days. | `number` | `30` | no | | billing\_data\_users | Google Workspace or Cloud Identity group that have access to billing data set. | `string` | n/a | yes | | billing\_export\_dataset\_location | The location of the dataset for billing data export. | `string` | `"US"` | no | +| cai\_monitoring\_kms\_force\_destroy | If set to true, delete KMS keyring and keys when destroying the module; otherwise, destroying the module will fail if KMS keys are present. | `bool` | `false` | no | | create\_access\_context\_manager\_access\_policy | Whether to create access context manager access policy. | `bool` | `true` | no | | create\_unique\_tag\_key | Creates unique organization-wide tag keys by adding a random suffix to each key. | `bool` | `false` | no | | data\_access\_logs\_enabled | Enable Data Access logs of types DATA\_READ, DATA\_WRITE for all GCP services. Enabling Data Access logs might result in your organization being charged for the additional logs usage. See https://cloud.google.com/logging/docs/audit#data-access The ADMIN\_READ logs are enabled by default. | `bool` | `false` | no | @@ -32,6 +33,10 @@ | Name | Description | |------|-------------| | base\_net\_hub\_project\_id | The Base Network hub project ID | +| cai\_monitoring\_artifact\_registry | CAI Monitoring Cloud Function Artifact Registry name. | +| cai\_monitoring\_asset\_feed | CAI Monitoring Cloud Function Organization Asset Feed name. | +| cai\_monitoring\_bucket | CAI Monitoring Cloud Function Source Bucket name. | +| cai\_monitoring\_topic | CAI Monitoring Cloud Function Pub/Sub Topic name. | | common\_folder\_name | The common folder name | | dns\_hub\_project\_id | The DNS hub project ID | | domains\_to\_allow | The list of domains to allow users from in IAM. | diff --git a/1-org/envs/shared/cai_monitoring.tf b/1-org/envs/shared/cai_monitoring.tf new file mode 100644 index 000000000..ff9311bfa --- /dev/null +++ b/1-org/envs/shared/cai_monitoring.tf @@ -0,0 +1,38 @@ +/** + * Copyright 2023 Google LLC + * + * 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. + */ + +module "kms" { + source = "terraform-google-modules/kms/google" + version = "~> 2.1" + + project_id = module.scc_notifications.project_id + keyring = "krg-cai-monitoring" + location = local.default_region + keys = ["key-cai-monitoring"] + prevent_destroy = !var.cai_monitoring_kms_force_destroy +} + +module "cai_monitoring" { + source = "../../modules/cai-monitoring" + + org_id = local.org_id + billing_account = local.billing_account + project_id = module.scc_notifications.project_id + location = local.default_region + enable_cmek = true + encryption_key = module.kms.keys["key-cai-monitoring"] + impersonate_sa_email = local.org_step_terraform_service_account_email +} diff --git a/1-org/envs/shared/main.tf b/1-org/envs/shared/main.tf index 2c463e80a..a32fc99aa 100644 --- a/1-org/envs/shared/main.tf +++ b/1-org/envs/shared/main.tf @@ -29,6 +29,7 @@ locals { group_billing_admins = data.terraform_remote_state.bootstrap.outputs.group_billing_admins group_org_admins = data.terraform_remote_state.bootstrap.outputs.group_org_admins networks_step_terraform_service_account_email = data.terraform_remote_state.bootstrap.outputs.networks_step_terraform_service_account_email + org_step_terraform_service_account_email = data.terraform_remote_state.bootstrap.outputs.organization_step_terraform_service_account_email bootstrap_folder_name = data.terraform_remote_state.bootstrap.outputs.common_config.bootstrap_folder_name cloud_build_private_worker_pool_id = try(data.terraform_remote_state.bootstrap.outputs.cloud_build_private_worker_pool_id, "") } diff --git a/1-org/envs/shared/outputs.tf b/1-org/envs/shared/outputs.tf index 725e26b2f..80aa4db83 100644 --- a/1-org/envs/shared/outputs.tf +++ b/1-org/envs/shared/outputs.tf @@ -118,3 +118,23 @@ output "tags" { value = local.tags_output description = "Tag Values to be applied on next steps" } + +output "cai_monitoring_artifact_registry" { + value = module.cai_monitoring.artifact_registry_name + description = "CAI Monitoring Cloud Function Artifact Registry name." +} + +output "cai_monitoring_asset_feed" { + value = module.cai_monitoring.asset_feed_name + description = "CAI Monitoring Cloud Function Organization Asset Feed name." +} + +output "cai_monitoring_bucket" { + value = module.cai_monitoring.bucket_name + description = "CAI Monitoring Cloud Function Source Bucket name." +} + +output "cai_monitoring_topic" { + value = module.cai_monitoring.topic_name + description = "CAI Monitoring Cloud Function Pub/Sub Topic name." +} diff --git a/1-org/envs/shared/projects.tf b/1-org/envs/shared/projects.tf index 942c9e1e8..37f9ee0ee 100644 --- a/1-org/envs/shared/projects.tf +++ b/1-org/envs/shared/projects.tf @@ -162,7 +162,7 @@ module "scc_notifications" { org_id = local.org_id billing_account = local.billing_account folder_id = google_folder.common.id - activate_apis = ["logging.googleapis.com", "pubsub.googleapis.com", "securitycenter.googleapis.com", "billingbudgets.googleapis.com"] + activate_apis = ["logging.googleapis.com", "pubsub.googleapis.com", "securitycenter.googleapis.com", "billingbudgets.googleapis.com", "cloudkms.googleapis.com"] labels = { environment = "production" diff --git a/1-org/envs/shared/variables.tf b/1-org/envs/shared/variables.tf index 82a210b29..12d702260 100644 --- a/1-org/envs/shared/variables.tf +++ b/1-org/envs/shared/variables.tf @@ -211,3 +211,9 @@ variable "create_unique_tag_key" { type = bool default = false } + +variable "cai_monitoring_kms_force_destroy" { + description = "If set to true, delete KMS keyring and keys when destroying the module; otherwise, destroying the module will fail if KMS keys are present." + type = bool + default = false +} diff --git a/1-org/modules/cai-monitoring/README.md b/1-org/modules/cai-monitoring/README.md new file mode 100644 index 000000000..98e53a951 --- /dev/null +++ b/1-org/modules/cai-monitoring/README.md @@ -0,0 +1,71 @@ +# Cloud Asset Inventory Notification +Uses Google Cloud Asset Inventory to create a feed of IAM Policy change events, then process them to detect when a roles (from a preset list) is given to a member (service account, user or group). Then generates a SCC Finding with the member, role, resource where it was granted and the time that was granted. + +## Usage + +```hcl +module "secure_cai_notification" { + source = "terraform-google-modules/terraform-example-foundation/google//1-org/modules/cai-monitoring" + + org_id = + billing_account = + project_id = + region = + encryption_key = + labels = + impersonate_sa_email = + roles_to_monitor = +} +``` + + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| billing\_account | The ID of the billing account to associate projects with. | `string` | n/a | yes | +| enable\_cmek | The KMS Key to Encrypt Artifact Registry repository, Cloud Storage Bucket and Pub/Sub. | `bool` | `false` | no | +| encryption\_key | The KMS Key to Encrypt Artifact Registry repository, Cloud Storage Bucket and Pub/Sub. | `string` | `null` | no | +| impersonate\_sa\_email | The Service Account email who will execute terraform code. | `string` | n/a | yes | +| labels | Labels to be assigned to resources. | `map(any)` | `{}` | no | +| location | Default location to create resources where applicable. | `string` | `"us-central1"` | no | +| org\_id | GCP Organization ID | `string` | n/a | yes | +| project\_id | The Project ID where the resources will be created | `string` | n/a | yes | +| random\_suffix | Adds a suffix of 4 random characters to the created resources names. | `bool` | `true` | no | +| roles\_to\_monitor | List of roles that will save a SCC Finding if granted to any member (service account, user or group) on an update in the IAM Policy. | `list(string)` |
[
"roles/owner",
"roles/editor",
"roles/resourcemanager.organizationAdmin",
"roles/compute.networkAdmin",
"roles/compute.orgFirewallPolicyAdmin"
]
| no | + +## Outputs + +| Name | Description | +|------|-------------| +| artifact\_registry\_name | Artifact Registry Repo to store the Cloud Function image. | +| asset\_feed\_name | Organization Asset Feed. | +| bucket\_name | Storage bucket where the source code is. | +| function\_uri | URI of the Cloud Function. | +| scc\_source | SCC Findings Source. | +| topic\_name | Pub/Sub Topic for the Asset Feed. | + + + +## Requirements + +### Software + +The following dependencies must be available: + +* [Terraform](https://www.terraform.io/downloads.html) >= 1.3 +* [Terraform Provider for GCP](https://github.com/terraform-providers/terraform-provider-google) < 5.0 + +### APIs + +A project with the following APIs enabled must be used to host the resources of this module: + +* Project + * Google Cloud Key Management Service: `cloudkms.googleapis.com` + * Cloud Resource Manager API: `cloudresourcemanager.googleapis.com` + * Cloud Functions API: `cloudfunctions.googleapis.com` + * Cloud Build API: `cloudbuild.googleapis.com` + * Cloud Asset API`cloudasset.googleapis.com` + * Clouod Pub/Sub API: `pubsub.googleapis.com` + * Identity and Access Management (IAM) API: `iam.googleapis.com` + * Cloud Billing API: `cloudbilling.googleapis.com` diff --git a/1-org/modules/cai-monitoring/function-source/index.js b/1-org/modules/cai-monitoring/function-source/index.js new file mode 100644 index 000000000..8f229fc4f --- /dev/null +++ b/1-org/modules/cai-monitoring/function-source/index.js @@ -0,0 +1,195 @@ +/** + * Copyright 2023 Google LLC + * + * 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. + */ + +'use strict' + +// Import +const uuid4 = require('uuid4') +const moment = require('moment') + +// SCC client +const { SecurityCenterClient } = require('@google-cloud/security-center'); +const client = new SecurityCenterClient(); + +// Environment variables +const sourceId = process.env.SOURCE_ID; +const searchroles = process.env.ROLES.split(","); + +// Variables +const sccConfig = { + category: 'PRIVILEGED_ROLE_GRANTED', + findingClass: 'VULNERABILITY', + severity: 'MEDIUM' +}; + +// Exported function +exports.caiMonitoring = message => { + try { + var event = parseMessage(message); + + // This validate is specific for the CAI Monitoring scenario. + // If you want to use this Cloud Function for other purpose, please change this validate function. + validateEvent(event); + + // From this part of the code until the end of the function is specific for the CAI Monitoring scenario + var bindings = getRoleBindings(event.asset); + + // If the list is not empty, search for the same member and role on the prior asset. + // Get only the new bindings that is not on the prior asset and create a new finding. + if (bindings.length > 0) { + var delta = bindingDiff(bindings, getRoleBindings(event.priorAsset)); + + if (delta.length > 0) { + // Map of extra properties to save on the finding with field name and value + var extraProperties = { + iamBindings: delta + }; + + createFinding( + event.asset.updateTime, + event.asset.name, + extraProperties + ); + } + } + } catch (error) { + console.warn(`Skipping executing with message: ${error.message}`); + } +} + +/** + * Parse the message received on the Cloud Function to a JSON. + * + * @param {any} message Message from Cloud Function + * @returns {JSON} Json object from the message + * @exception If some error happens while parsing, it will log the error and finish the execution + */ +function parseMessage(message) { + // If message data is missing, log a warning and exit. + if (!(message && message.data)) { + throw new Error(`Missing required fields (message or message.data)`); + } + + // Extract the event data from the message + var event = JSON.parse(Buffer.from(message.data, 'base64').toString()); + + return event; +} + +/** + * Validate if the asset is from Organizations and have the iamPolicy and bindings field. + * + * @param {any} asset Asset JSON. + * @exception If the asset is not valid it will throw the corresponding error. + */ +function validateEvent(event) { + // If the asset is not present, throw an error. + if (!(event.asset && event.asset.iamPolicy && event.asset.iamPolicy.bindings)) { + throw new Error(`Missing required fields (asset or asset.iamPolicy or asset.iamPolicy.bindings)`); + } + + // If event priorAsset is missing and assetType is Project, is a new project creation, log a warning and exit + if (!(event.priorAsset && event.priorAsset.iamPolicy) && event.asset.assetType === "cloudresourcemanager.googleapis.com/Project") { + var name = event.asset.name.split("/"); + throw new Error(`Creating project ${name[name.length - 1]}, prior asset is empty`); + } +} + +/** + * Return an array of all members that have overprivileged roles. + * If there's no member, it will return an empty array. + * + * @param {Asset} asset The asset to find members with selected permissions + * @returns {Array} The array of found bindings ({member: String, role: String, action: String('ADD')}) sorted by role + */ +function getRoleBindings(asset) { + try { + var foundRoles = []; + var bindings = asset.iamPolicy.bindings; + + // Check for bindings that include the list of roles + bindings.forEach(binding => { + if (searchroles.includes(binding.role)) { + binding.members.forEach(member => { + foundRoles.push({ + member: member, + role: binding.role, + action: 'ADD' + }); + }); + } + }); + + return foundRoles; + } catch (error) { + console.warn(`Returning empty bindings with message: ${error.message}`); + return []; + } +} + +/** + * Return an array of the members difference between the actual and the prior bindings. + * If there's no member, it will return an empty array. + * + * @param {Array} bindings Array of the actual binding + * @param {Array} priorBindings Array of the prior binding + * @returns {Array} The difference array between actual and prior bindings + */ +function bindingDiff(bindings, priorBindings) { + return bindings.filter(actual => !priorBindings.some(prior => (prior.member === actual.member && prior.role === actual.role))); +} + +/** + * Convert string date to google.protobuf.Timestamp format + * + * @param {string} dateTimeStr date time format as a String. (e.g. 2019-02-15T10:23:13Z) + */ +function parseStrTime(dateTimeStr) { + const dateTimeStrInMillis = moment.utc(dateTimeStr).valueOf() + return { + seconds: Math.trunc(dateTimeStrInMillis / 1000), + nanos: Math.trunc((dateTimeStrInMillis % 1000) * 1000000) + } +} + +/** + * Create the new SCC finding + * + * @param {string} updateTime The time that the asset was changed. + * @param {string} resourceName The resource where the role was given. + * @param {Any} extraProperties A key/value map with properties to save on the finding ({fieldName: fieldValue}) + */ +async function createFinding(updateTime, resourceName, extraProperties) { + const [newFinding] = await client.createFinding( + { + parent: sourceId, + findingId: uuid4().replace(/-/g, ''), + finding: { + ... { + state: 'ACTIVE', + resourceName: resourceName, + category: sccConfig.category, + eventTime: parseStrTime(updateTime), + findingClass: sccConfig.findingClass, + severity: sccConfig.severity + }, + ...extraProperties + } + } + ); + + console.log('New finding created: %j', newFinding); +} diff --git a/1-org/modules/cai-monitoring/function-source/package-lock.json b/1-org/modules/cai-monitoring/function-source/package-lock.json new file mode 100644 index 000000000..8301d0360 --- /dev/null +++ b/1-org/modules/cai-monitoring/function-source/package-lock.json @@ -0,0 +1,856 @@ +{ + "name": "revertgmailiamgrant", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@google-cloud/paginator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.0.tgz", + "integrity": "sha512-87aeg6QQcEPxGCOthnpUjvw4xAZ57G7pL8FS0C4e/81fr3FjkpUpibf1s2v5XGyGhUVGF4Jfg7yEcxqn2iUw1w==", + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + } + }, + "@google-cloud/precise-date": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-4.0.0.tgz", + "integrity": "sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA==" + }, + "@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==" + }, + "@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==" + }, + "@google-cloud/pubsub": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-4.0.6.tgz", + "integrity": "sha512-F67FvIAjARqdB9OF5Vy8sOQxfQyzTlTsjuNnJPvpCqv/4LlWwsfnW4FcCLa+Wlb92vaLI3ikHxLrqYz6WWg2cA==", + "requires": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/precise-date": "^4.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "@opentelemetry/api": "^1.6.0", + "@opentelemetry/semantic-conventions": "~1.17.0", + "@types/duplexify": "^3.6.0", + "@types/long": "^4.0.0", + "arrify": "^2.0.0", + "extend": "^3.0.2", + "google-auth-library": "^9.0.0", + "google-gax": "^4.0.4", + "heap-js": "^2.2.0", + "is-stream-ended": "^0.1.4", + "lodash.snakecase": "^4.1.1", + "p-defer": "^3.0.0" + }, + "dependencies": { + "agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "requires": { + "debug": "^4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } + } + }, + "gaxios": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.1.1.tgz", + "integrity": "sha512-bw8smrX+XlAoo9o1JAksBwX+hi/RG15J+NTSxmNPIclKC3ZVK6C2afwY8OSdRvOK0+ZLecUJYtj2MmjOt3Dm0w==", + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + } + }, + "gcp-metadata": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.0.0.tgz", + "integrity": "sha512-Ozxyi23/1Ar51wjUT2RDklK+3HxqDr8TLBNK8rBBFQ7T85iIGnXnVusauj06QyqCXRFZig8LZC+TUddWbndlpQ==", + "requires": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.1.0.tgz", + "integrity": "sha512-1M9HdOcQNPV5BwSXqwwT238MTKodJFBxZ/V2JP397ieOLv4FjQdfYb9SooR7Mb+oUT2IJ92mLJQf804dyx0MJA==", + "requires": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.0.0", + "gcp-metadata": "^6.0.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "gtoken": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.0.1.tgz", + "integrity": "sha512-KcFVtoP1CVFtQu0aSk3AyAt2og66PFhZAlkUOuWKwzMLoulHXG5W5wE5xAnHb+yl3/wEFoqGW7/cDGMU8igDZQ==", + "requires": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + } + }, + "https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "requires": { + "whatwg-url": "^5.0.0" + } + } + } + }, + "@grpc/grpc-js": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.6.tgz", + "integrity": "sha512-yq3qTy23u++8zdvf+h4mz4ohDFi681JAkMZZPTKh8zmUVh0AKLisFlgxcn22FMNowXz15oJ6pqgwT7DJ+PdJvg==", + "requires": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + } + }, + "@grpc/proto-loader": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", + "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "requires": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + } + }, + "@opentelemetry/api": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.6.0.tgz", + "integrity": "sha512-OWlrQAnWn9577PhVgqjUvMr1pg57Bc4jv0iL4w0PRuOSRvq67rvHW9Ie/dZVMvCzhSCB+UxhcY/PmCmFj33Q+g==" + }, + "@opentelemetry/semantic-conventions": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.17.1.tgz", + "integrity": "sha512-xbR2U+2YjauIuo42qmE8XyJK6dYeRMLJuOlUP5SO4auET4VtOHOzgkRVOq+Ik18N+Xf3YPcqJs9dZMiDddz1eQ==" + }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==" + }, + "@types/caseless": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.4.tgz", + "integrity": "sha512-2in/lrHRNmDvHPgyormtEralhPcN3An1gLjJzj2Bw145VBxkQ75JEXW6CTdMAwShiHQcYsl2d10IjQSdJSJz4g==" + }, + "@types/duplexify": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.3.tgz", + "integrity": "sha512-KE0Yb3JraglJMB53+A/RMXbd9w//pQfiSqkrsoAxKcNOEIe1EHfEgbvoi2lkk2AvhhJtplugJSB2Mptc3DZMNA==", + "requires": { + "@types/node": "*" + } + }, + "@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "@types/node": { + "version": "20.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", + "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==", + "requires": { + "undici-types": "~5.25.1" + } + }, + "@types/request": { + "version": "2.48.11", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.11.tgz", + "integrity": "sha512-HuihY1+Vss5RS9ZHzRyTGIzwPTdrJBkCm/mAeLRYrOQu/MGqyezKXWOK1VhCnR+SDbp9G2mRUP+OVEqCrzpcfA==", + "requires": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "@types/tough-cookie": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.4.tgz", + "integrity": "sha512-95Sfz4nvMAb0Nl9DTxN3j64adfwfbBPEYq14VN7zT5J5O2M9V6iZMIIQU1U+pJyl9agHYHNCqhCXgyEtIRRa5A==" + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "agent-base": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", + "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", + "requires": { + "debug": "4" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "google-gax": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.0.5.tgz", + "integrity": "sha512-yLoYtp4zE+8OQA74oBEbNkbzI6c95W01JSL7RqC8XERKpRvj3ytZp1dgnbA6G9aRsc8pZB25xWYBcCmrbYOEhA==", + "requires": { + "@grpc/grpc-js": "~1.9.6", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.0.0", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.0", + "protobufjs": "7.2.5", + "retry-request": "^7.0.0" + }, + "dependencies": { + "agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "requires": { + "debug": "^4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } + } + }, + "gaxios": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.1.1.tgz", + "integrity": "sha512-bw8smrX+XlAoo9o1JAksBwX+hi/RG15J+NTSxmNPIclKC3ZVK6C2afwY8OSdRvOK0+ZLecUJYtj2MmjOt3Dm0w==", + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + } + }, + "gcp-metadata": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.0.0.tgz", + "integrity": "sha512-Ozxyi23/1Ar51wjUT2RDklK+3HxqDr8TLBNK8rBBFQ7T85iIGnXnVusauj06QyqCXRFZig8LZC+TUddWbndlpQ==", + "requires": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.1.0.tgz", + "integrity": "sha512-1M9HdOcQNPV5BwSXqwwT238MTKodJFBxZ/V2JP397ieOLv4FjQdfYb9SooR7Mb+oUT2IJ92mLJQf804dyx0MJA==", + "requires": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.0.0", + "gcp-metadata": "^6.0.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "gtoken": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.0.1.tgz", + "integrity": "sha512-KcFVtoP1CVFtQu0aSk3AyAt2og66PFhZAlkUOuWKwzMLoulHXG5W5wE5xAnHb+yl3/wEFoqGW7/cDGMU8igDZQ==", + "requires": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + } + }, + "https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "requires": { + "whatwg-url": "^5.0.0" + } + } + } + }, + "heap-js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.3.0.tgz", + "integrity": "sha512-E5303mzwQ+4j/n2J0rDvEPBN7GKjhis10oHiYOgjxsmxYgqG++hz9NyLLOXttzH8as/DyiBHYpUrJTZWYaMo8Q==" + }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" + }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + }, + "long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==" + }, + "proto3-json-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.0.tgz", + "integrity": "sha512-FB/YaNrpiPkyQNSNPilpn8qn0KdEfkgmJ9JP93PQyF/U4bAiXY5BiUdDhiDO4S48uSQ6AesklgVlrKiqZPzegw==", + "requires": { + "protobufjs": "^7.0.0" + } + }, + "protobufjs": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "retry-request": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.1.tgz", + "integrity": "sha512-ZI6vJp9rfB71mrZpw+n9p/B6HCsd7QJlSEQftZ+xfJzr3cQ9EPGKw1FF0BnViJ0fYREX6FhymBD2CARpmsFciQ==", + "requires": { + "@types/request": "^2.48.8", + "debug": "^4.1.1", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "requires": { + "stubs": "^3.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==" + }, + "teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "requires": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "dependencies": { + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + } + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "undici-types": { + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", + "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + } + } +} diff --git a/1-org/modules/cai-monitoring/function-source/package.json b/1-org/modules/cai-monitoring/function-source/package.json new file mode 100644 index 000000000..95e877373 --- /dev/null +++ b/1-org/modules/cai-monitoring/function-source/package.json @@ -0,0 +1,16 @@ +{ + "name": "caiMonitoring", + "version": "1.0.0", + "description": "A Cloud Function that receives events from a Pub/Sub Subscription fed by a Cloud Asset Inventory IAM event feed, and then notify if the IAM Policy grants roles from a predefined set to a new member.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@google-cloud/security-center": "8.1.0", + "uuid4": "2.0.3", + "moment": "2.29.4" + } +} diff --git a/1-org/modules/cai-monitoring/iam.tf b/1-org/modules/cai-monitoring/iam.tf new file mode 100644 index 000000000..ccb9959d6 --- /dev/null +++ b/1-org/modules/cai-monitoring/iam.tf @@ -0,0 +1,86 @@ +/** + * Copyright 2023 Google LLC + * + * 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. + */ + +locals { + cf_roles = [ + "roles/pubsub.publisher", + "roles/eventarc.eventReceiver", + "roles/run.invoker" + ] + services = { + "cloudfunctions" = "cloudfunctions.googleapis.com" + "artifactregistry" = "artifactregistry.googleapis.com" + "pubsub" = "pubsub.googleapis.com" + } + identities = { + "cloudfunctions" = "serviceAccount:${google_project_service_identity.service_sa["cloudfunctions"].email}", + "artifactregistry" = "serviceAccount:${google_project_service_identity.service_sa["artifactregistry"].email}", + "pubsub" = "serviceAccount:${google_project_service_identity.service_sa["pubsub"].email}", + "storage" = "serviceAccount:${data.google_storage_project_service_account.gcs_sa.email_address}" + } +} + +// Service Accounts +resource "google_project_service_identity" "service_sa" { + for_each = local.services + provider = google-beta + + project = var.project_id + service = each.value +} + +data "google_storage_project_service_account" "gcs_sa" { + project = var.project_id +} + +// Encrypter/Decrypter role +resource "google_kms_crypto_key_iam_member" "encrypter_decrypter" { + for_each = var.enable_cmek ? local.identities : {} + + crypto_key_id = var.encryption_key + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + member = each.value +} + +// Cloud Function SA +resource "google_service_account" "cloudfunction" { + account_id = "cai-monitoring" + project = var.project_id +} + +resource "google_organization_iam_member" "cloudfunction_findings_editor" { + org_id = var.org_id + role = "roles/securitycenter.findingsEditor" + member = "serviceAccount:${google_service_account.cloudfunction.email}" +} + +resource "google_project_iam_member" "cloudfunction_iam" { + for_each = toset(local.cf_roles) + + project = var.project_id + role = each.key + member = "serviceAccount:${google_service_account.cloudfunction.email}" +} + +// Time sleep +resource "time_sleep" "wait_kms_iam" { + create_duration = "60s" + depends_on = [ + google_kms_crypto_key_iam_member.encrypter_decrypter, + google_organization_iam_member.cloudfunction_findings_editor, + google_project_iam_member.cloudfunction_iam + ] +} diff --git a/1-org/modules/cai-monitoring/main.tf b/1-org/modules/cai-monitoring/main.tf new file mode 100644 index 000000000..b31da358e --- /dev/null +++ b/1-org/modules/cai-monitoring/main.tf @@ -0,0 +1,181 @@ +/** + * Copyright 2023 Google LLC + * + * 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. + */ + +locals { + project_service_apis = [ + "cloudresourcemanager.googleapis.com", + "cloudfunctions.googleapis.com", + "cloudbuild.googleapis.com", + "cloudasset.googleapis.com", + "pubsub.googleapis.com", + "artifactregistry.googleapis.com", + "storage.googleapis.com", + "run.googleapis.com", + "eventarc.googleapis.com" + ] + cai_source_name = var.random_suffix ? "CAI Monitoring - ${random_id.suffix.hex}" : "CAI Monitoring" +} + +data "google_project" "project" { + project_id = var.project_id +} + +resource "random_id" "suffix" { + byte_length = 2 +} + +// Project Services +resource "google_project_service" "services" { + for_each = toset(local.project_service_apis) + + project = var.project_id + service = each.key + disable_on_destroy = false +} + +// Artifact Registry +resource "google_artifact_registry_repository" "cloudfunction" { + location = var.location + project = var.project_id + repository_id = "ar-cai-monitoring-${random_id.suffix.hex}" + description = "This repo stores de image of the cloud function." + format = "DOCKER" + kms_key_name = var.encryption_key + labels = var.labels + + depends_on = [ + time_sleep.wait_kms_iam + ] +} + +// Bucket and function source code +data "archive_file" "function_source_zip" { + type = "zip" + source_dir = "${path.module}/function-source/" + output_path = "${path.module}/cai_monitoring.zip" + excludes = ["package-lock.json"] +} + +module "cloudfunction_source_bucket" { + source = "terraform-google-modules/cloud-storage/google//modules/simple_bucket" + version = "~>3.4" + + project_id = var.project_id + name = "bkt-cai-monitoring-${random_id.suffix.hex}-sources-${data.google_project.project.number}-${var.location}" + location = var.location + storage_class = "REGIONAL" + force_destroy = true + + encryption = var.enable_cmek ? { + default_kms_key_name = var.encryption_key + } : null + + depends_on = [ + time_sleep.wait_kms_iam + ] +} + +resource "time_sleep" "wait_for_bucket" { + depends_on = [module.cloudfunction_source_bucket] + create_duration = "30s" +} + +resource "google_storage_bucket_object" "cf_cai_source_zip" { + name = "${path.module}/cai_monitoring.zip" + bucket = module.cloudfunction_source_bucket.name + source = data.archive_file.function_source_zip.output_path + + depends_on = [ + time_sleep.wait_for_bucket + ] +} + +// PubSub +resource "google_cloud_asset_organization_feed" "organization_feed" { + feed_id = "fd-cai-monitoring-${random_id.suffix.hex}" + billing_project = var.project_id + org_id = var.org_id + content_type = "IAM_POLICY" + + asset_types = [".*"] + + feed_output_config { + pubsub_destination { + topic = module.pubsub_cai_feed.id + } + } +} + +module "pubsub_cai_feed" { + source = "terraform-google-modules/pubsub/google" + version = "~> 5.0" + + topic = "top-cai-monitoring-${random_id.suffix.hex}-event" + project_id = var.project_id + topic_kms_key_name = var.encryption_key + + depends_on = [ + time_sleep.wait_kms_iam + ] +} + +// SCC source +resource "google_scc_source" "cai_monitoring" { + display_name = local.cai_source_name + organization = var.org_id + description = "SCC Finding Source for caiMonitoring Cloud Functions." +} + +// Cloud Function +module "cloud_function" { + source = "GoogleCloudPlatform/cloud-functions/google" + version = "0.4.1" + + function_name = "caiMonitoring" + description = "Check on the Organization for members (users, groups and service accounts) that contains the IAM roles listed." + project_id = var.project_id + labels = var.labels + function_location = var.location + runtime = "nodejs20" + entrypoint = "caiMonitoring" + docker_repository = google_artifact_registry_repository.cloudfunction.id + + storage_source = { + bucket = module.cloudfunction_source_bucket.name + object = google_storage_bucket_object.cf_cai_source_zip.name + } + + service_config = { + service_account_email = google_service_account.cloudfunction.email + runtime_env_variables = { + ROLES = join(",", var.roles_to_monitor) + SOURCE_ID = google_scc_source.cai_monitoring.id + } + } + + event_trigger = { + trigger_region = var.location + event_type = "google.cloud.pubsub.topic.v1.messagePublished" + pubsub_topic = module.pubsub_cai_feed.id + retry_policy = "RETRY_POLICY_RETRY" + service_account_email = google_service_account.cloudfunction.email + } + + depends_on = [ + google_storage_bucket_object.cf_cai_source_zip, + time_sleep.wait_kms_iam + ] +} diff --git a/1-org/modules/cai-monitoring/outputs.tf b/1-org/modules/cai-monitoring/outputs.tf new file mode 100644 index 000000000..c3c4207a3 --- /dev/null +++ b/1-org/modules/cai-monitoring/outputs.tf @@ -0,0 +1,45 @@ +/** + * Copyright 2023 Google LLC + * + * 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. + */ + +output "artifact_registry_name" { + description = "Artifact Registry Repo to store the Cloud Function image." + value = google_artifact_registry_repository.cloudfunction.name +} + +output "bucket_name" { + description = "Storage bucket where the source code is." + value = module.cloudfunction_source_bucket.name +} + +output "asset_feed_name" { + description = "Organization Asset Feed." + value = google_cloud_asset_organization_feed.organization_feed.id +} + +output "topic_name" { + description = "Pub/Sub Topic for the Asset Feed." + value = module.pubsub_cai_feed.topic +} + +output "scc_source" { + description = "SCC Findings Source." + value = google_scc_source.cai_monitoring.id +} + +output "function_uri" { + description = "URI of the Cloud Function." + value = module.cloud_function.function_uri +} diff --git a/1-org/modules/cai-monitoring/providers.tf b/1-org/modules/cai-monitoring/providers.tf new file mode 100644 index 000000000..a801ce2f3 --- /dev/null +++ b/1-org/modules/cai-monitoring/providers.tf @@ -0,0 +1,25 @@ +/** + * Copyright 2023 Google LLC + * + * 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. + */ + +provider "google" { + impersonate_service_account = var.impersonate_sa_email + request_timeout = "60s" +} + +provider "google-beta" { + impersonate_service_account = var.impersonate_sa_email + request_timeout = "60s" +} diff --git a/1-org/modules/cai-monitoring/variables.tf b/1-org/modules/cai-monitoring/variables.tf new file mode 100644 index 000000000..a6652cc8d --- /dev/null +++ b/1-org/modules/cai-monitoring/variables.tf @@ -0,0 +1,77 @@ +/** + * Copyright 2023 Google LLC + * + * 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. + */ + +variable "org_id" { + description = "GCP Organization ID" + type = string +} + +variable "billing_account" { + description = "The ID of the billing account to associate projects with." + type = string +} + +variable "project_id" { + description = "The Project ID where the resources will be created" + type = string +} + +variable "location" { + description = "Default location to create resources where applicable." + type = string + default = "us-central1" +} + +variable "enable_cmek" { + description = "The KMS Key to Encrypt Artifact Registry repository, Cloud Storage Bucket and Pub/Sub." + type = bool + default = false +} + +variable "encryption_key" { + description = "The KMS Key to Encrypt Artifact Registry repository, Cloud Storage Bucket and Pub/Sub." + type = string + default = null +} + +variable "labels" { + description = "Labels to be assigned to resources." + type = map(any) + default = {} +} + +variable "impersonate_sa_email" { + description = "The Service Account email who will execute terraform code." + type = string +} + +variable "roles_to_monitor" { + description = "List of roles that will save a SCC Finding if granted to any member (service account, user or group) on an update in the IAM Policy." + type = list(string) + default = [ + "roles/owner", + "roles/editor", + "roles/resourcemanager.organizationAdmin", + "roles/compute.networkAdmin", + "roles/compute.orgFirewallPolicyAdmin" + ] +} + +variable "random_suffix" { + description = "Adds a suffix of 4 random characters to the created resources names." + type = bool + default = true +} diff --git a/1-org/modules/cai-monitoring/versions.tf b/1-org/modules/cai-monitoring/versions.tf new file mode 100644 index 000000000..328699bc4 --- /dev/null +++ b/1-org/modules/cai-monitoring/versions.tf @@ -0,0 +1,32 @@ +/** + * Copyright 2023 Google LLC + * + * 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. + */ + +terraform { + required_version = ">= 1.3" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 3.77" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 3.77" + } + random = { + source = "hashicorp/random" + } + } +} diff --git a/helpers/foundation-deployer/global.tfvars.example b/helpers/foundation-deployer/global.tfvars.example index ac20260a4..de2bfec27 100644 --- a/helpers/foundation-deployer/global.tfvars.example +++ b/helpers/foundation-deployer/global.tfvars.example @@ -92,6 +92,7 @@ domains_to_allow = ["example.com"] # Must include the domain essential_contacts_domains_to_allow = ["@example.com"] scc_notification_name = "scc-notify" +cai_monitoring_kms_force_destroy = false audit_logs_table_delete_contents_on_destroy = false log_export_storage_force_destroy = false log_export_storage_location = "US" diff --git a/helpers/foundation-deployer/stages/apply.go b/helpers/foundation-deployer/stages/apply.go index 3735c0e89..f2fd3da14 100644 --- a/helpers/foundation-deployer/stages/apply.go +++ b/helpers/foundation-deployer/stages/apply.go @@ -202,6 +202,7 @@ func DeployOrgStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outputs Bo EnableHubAndSpoke: tfvars.EnableHubAndSpoke, CreateACMAPolicy: createACMAPolicy, CreateUniqueTagKey: tfvars.CreateUniqueTagKey, + CaiMonitoringKmsForceDestroy: tfvars.CaiMonitoringKmsForceDestroy, AuditLogsTableDeleteContentsOnDestroy: tfvars.AuditLogsTableDeleteContentsOnDestroy, LogExportStorageForceDestroy: tfvars.LogExportStorageForceDestroy, LogExportStorageLocation: tfvars.LogExportStorageLocation, diff --git a/helpers/foundation-deployer/stages/data.go b/helpers/foundation-deployer/stages/data.go index 4f1e134ba..f76f3a008 100644 --- a/helpers/foundation-deployer/stages/data.go +++ b/helpers/foundation-deployer/stages/data.go @@ -154,6 +154,7 @@ type GlobalTFVars struct { SccNotificationName string `hcl:"scc_notification_name"` ProjectPrefix *string `hcl:"project_prefix"` FolderPrefix *string `hcl:"folder_prefix"` + CaiMonitoringKmsForceDestroy *bool `hcl:"cai_monitoring_kms_force_destroy"` BucketForceDestroy *bool `hcl:"bucket_force_destroy"` AuditLogsTableDeleteContentsOnDestroy *bool `hcl:"audit_logs_table_delete_contents_on_destroy"` LogExportStorageForceDestroy *bool `hcl:"log_export_storage_force_destroy"` @@ -216,6 +217,7 @@ type OrgTfvars struct { EnableHubAndSpoke bool `hcl:"enable_hub_and_spoke"` CreateACMAPolicy bool `hcl:"create_access_context_manager_access_policy"` CreateUniqueTagKey bool `hcl:"create_unique_tag_key"` + CaiMonitoringKmsForceDestroy *bool `hcl:"cai_monitoring_kms_force_destroy"` AuditLogsTableDeleteContentsOnDestroy *bool `hcl:"audit_logs_table_delete_contents_on_destroy"` LogExportStorageForceDestroy *bool `hcl:"log_export_storage_force_destroy"` LogExportStorageLocation string `hcl:"log_export_storage_location"` diff --git a/helpers/foundation-deployer/stages/validate.go b/helpers/foundation-deployer/stages/validate.go index 4362de41d..3bc9dc4b5 100644 --- a/helpers/foundation-deployer/stages/validate.go +++ b/helpers/foundation-deployer/stages/validate.go @@ -105,6 +105,9 @@ func ValidateDestroyFlags(t testing.TB, g GlobalTFVars) { if g.LogExportStorageForceDestroy == nil || !*g.LogExportStorageForceDestroy { flags = append(flags, "log_export_storage_force_destroy") } + if g.CaiMonitoringKmsForceDestroy == nil || !*g.CaiMonitoringKmsForceDestroy { + flags = append(flags, "cai_monitoring_kms_force_destroy") + } if len(flags) > 0 { fmt.Println("# To use the feature to destroy the deployment created by this helper,") diff --git a/policy-library/policies/constraints/serviceusage_allow_basic_apis.yaml b/policy-library/policies/constraints/serviceusage_allow_basic_apis.yaml index af5c59978..d50935bfd 100644 --- a/policy-library/policies/constraints/serviceusage_allow_basic_apis.yaml +++ b/policy-library/policies/constraints/serviceusage_allow_basic_apis.yaml @@ -66,3 +66,7 @@ spec: - "storage-component.googleapis.com" - "workflows.googleapis.com" - "assuredworkloads.googleapis.com" + - "cloudfunctions.googleapis.com" + - "storage.googleapis.com" + - "run.googleapis.com" + - "eventarc.googleapis.com" diff --git a/test/integration/org/org_test.go b/test/integration/org/org_test.go index d56752aa7..5df12cd80 100644 --- a/test/integration/org/org_test.go +++ b/test/integration/org/org_test.go @@ -42,6 +42,7 @@ func TestOrg(t *testing.T) { "remote_state_bucket": backend_bucket, "log_export_storage_force_destroy": "true", "audit_logs_table_delete_contents_on_destroy": "true", + "cai_monitoring_kms_force_destroy": "true", } backendConfig := map[string]interface{}{ @@ -232,6 +233,38 @@ func TestOrg(t *testing.T) { "logName: /logs/cloudaudit.googleapis.com%2Faccess_transparency", } + // CAI Monitoring + // Variables + caiAr := org.GetStringOutput("cai_monitoring_artifact_registry") + caiBucket := org.GetStringOutput("cai_monitoring_bucket") + caiTopic := org.GetStringOutput("cai_monitoring_topic") + + caiSaEmail := fmt.Sprintf("cai-monitoring@%s.iam.gserviceaccount.com", sccProjectID) + caiKmsKey := fmt.Sprintf("projects/%s/locations/%s/keyRings/krg-cai-monitoring/cryptoKeys/key-cai-monitoring", sccProjectID, defaultRegion) + caiTopicFullName := fmt.Sprintf("projects/%s/topics/%s", sccProjectID, caiTopic) + + // Cloud Function + opCf := gcloud.Runf(t, "functions describe caiMonitoring --project %s --gen2 --region %s", sccProjectID, defaultRegion) + assert.Equal("ACTIVE", opCf.Get("state").String(), "Should be ACTIVE. Cloud Function is not successfully deployed.") + assert.Equal(caiSaEmail, opCf.Get("serviceConfig.serviceAccountEmail").String(), fmt.Sprintf("Cloud Function should use the service account %s.", caiSaEmail)) + assert.Contains(opCf.Get("eventTrigger.eventType").String(), "google.cloud.pubsub.topic.v1.messagePublished", "Event Trigger is not based on Pub/Sub message. Check the EventType configuration.") + + // Cloud Function Storage Bucket + bktArgs := gcloud.WithCommonArgs([]string{"--project", sccProjectID, "--json"}) + opSrcBucket := gcloud.Run(t, fmt.Sprintf("alpha storage ls --buckets gs://%s", caiBucket), bktArgs).Array() + assert.Equal(caiKmsKey, opSrcBucket[0].Get("metadata.encryption.defaultKmsKeyName").String(), fmt.Sprintf("Should have same KMS key: %s", caiKmsKey)) + assert.Equal("true", opSrcBucket[0].Get("metadata.iamConfiguration.bucketPolicyOnly.enabled").String(), "Should have Bucket Policy Only enabled.") + + // Cloud Function Artifact Registry + opAR := gcloud.Runf(t, "artifacts repositories describe %s --project %s --location %s", caiAr, sccProjectID, defaultRegion) + assert.Equal(caiKmsKey, opAR.Get("kmsKeyName").String(), fmt.Sprintf("Should have KMS Key: %s", caiKmsKey)) + assert.Equal("DOCKER", opAR.Get("format").String(), "Should have type: DOCKER") + + // Cloud Function Pub/Sub + opTopic := gcloud.Runf(t, "pubsub topics describe %s --project %s", caiTopic, sccProjectID) + assert.Equal(caiTopicFullName, opTopic.Get("name").String(), fmt.Sprintf("Topic %s should have been created", caiTopicFullName)) + + // Log Sink for _, sink := range []struct { name string hasFilter bool