From f626b8b17b60fc38c96fc6bcdd7740d258fcdbc6 Mon Sep 17 00:00:00 2001 From: Niel Markwick Date: Thu, 12 Sep 2024 18:03:57 +0200 Subject: [PATCH] feat: initial attempt at terraforming --- .gitignore | 7 ++ cloudrun-malware-scanner/.gcloudignore | 1 + cloudrun-malware-scanner/cloudbuild.yaml | 28 +++++ terraform/README.md | 0 terraform/infra/apis/main.tf | 36 ++++++ terraform/infra/demo_buckets/main.tf | 44 +++++++ terraform/infra/main.tf | 113 +++++++++++++++++ terraform/infra/variables.tf | 52 ++++++++ terraform/infra/versions.tf | 27 ++++ terraform/service/main.tf | 149 +++++++++++++++++++++++ terraform/service/variables.tf | 30 +++++ terraform/service/versions.tf | 31 +++++ 12 files changed, 518 insertions(+) create mode 100644 cloudrun-malware-scanner/cloudbuild.yaml create mode 100644 terraform/README.md create mode 100644 terraform/infra/apis/main.tf create mode 100644 terraform/infra/demo_buckets/main.tf create mode 100644 terraform/infra/main.tf create mode 100644 terraform/infra/variables.tf create mode 100644 terraform/infra/versions.tf create mode 100644 terraform/service/main.tf create mode 100644 terraform/service/variables.tf create mode 100644 terraform/service/versions.tf diff --git a/.gitignore b/.gitignore index 03cddf9c..eabb11e8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,10 @@ cvds pyenv config.json .vscode + +# Terraform +*.tfstate +*.tfstate.backup +*.tfstate.lock.info +*.tfplan +.terraform diff --git a/cloudrun-malware-scanner/.gcloudignore b/cloudrun-malware-scanner/.gcloudignore index da729999..598b49f0 100644 --- a/cloudrun-malware-scanner/.gcloudignore +++ b/cloudrun-malware-scanner/.gcloudignore @@ -3,3 +3,4 @@ pyenv node_modules .gcloudignore .eslintrc.js +config.json.tmpl diff --git a/cloudrun-malware-scanner/cloudbuild.yaml b/cloudrun-malware-scanner/cloudbuild.yaml new file mode 100644 index 00000000..1907d6da --- /dev/null +++ b/cloudrun-malware-scanner/cloudbuild.yaml @@ -0,0 +1,28 @@ +# Copyright 2024 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. + +steps: + - name: "gcr.io/cloud-builders/docker" + args: + [ + "build", + "--tag=$REGION-docker.pkg.dev/$PROJECT_ID/malware-scanner/malware-scanner:latest", + "-f", + "Dockerfile", + ".", + ] +images: + - "$REGION-docker.pkg.dev/$PROJECT_ID/malware-scanner/malware-scanner:latest" +options: + logging: "CLOUD_LOGGING_ONLY" diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 00000000..e69de29b diff --git a/terraform/infra/apis/main.tf b/terraform/infra/apis/main.tf new file mode 100644 index 00000000..88e9ba06 --- /dev/null +++ b/terraform/infra/apis/main.tf @@ -0,0 +1,36 @@ +# Copyright 2024 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 +# +# https://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. + +# Enable APIs + +# Requires that the following APIs are already enabled: +# cloudresourcemanager.googleapis.com +# serviceusage.googleapis.com + +locals { + apis = [ + "artifactregistry.googleapis.com", + "run.googleapis.com", + "eventarc.googleapis.com", + "logging.googleapis.com", + "cloudbuild.googleapis.com", + "cloudscheduler.googleapis.com", + "pubsub.googleapis.com", + ] +} +resource "google_project_service" "apis" { + for_each = toset(local.apis) + service = each.key +} + diff --git a/terraform/infra/demo_buckets/main.tf b/terraform/infra/demo_buckets/main.tf new file mode 100644 index 00000000..97e3f0b5 --- /dev/null +++ b/terraform/infra/demo_buckets/main.tf @@ -0,0 +1,44 @@ +# Copyright 2024 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 +# +# https://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 "projectId" { +} +variable "service_account_email" { +} +variable "location" { +} +variable "uniform_bucket_level_access" { +} + +locals { + buckets = toset(["unscanned", "quarantined", "clean"]) +} + + +resource "google_storage_bucket" "demo_buckets" { + for_each = local.buckets + name = "${each.key}-${var.projectId}" + location = var.location + uniform_bucket_level_access = var.uniform_bucket_level_access +} + +resource "google_storage_bucket_iam_binding" "demo_buckets_sa_binding" { + for_each = google_storage_bucket.demo_buckets + bucket = each.value.name + role = "roles/storage.admin" + members = [ + "serviceAccount:${var.service_account_email}", + ] +} diff --git a/terraform/infra/main.tf b/terraform/infra/main.tf new file mode 100644 index 00000000..dce6974e --- /dev/null +++ b/terraform/infra/main.tf @@ -0,0 +1,113 @@ +# Copyright 2021 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 +# +# https://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" { + project = var.projectId + region = var.region +} + +locals { + repo_root = abspath("${path.module}/../..") + src_root = abspath("${local.repo_root}/cloudrun-malware-scanner") +} + +module "apis" { + source = "./apis" + count = var.enable_apis ? 1 : 0 +} + +resource "google_service_account" "malware_scanner_sa" { + account_id = var.service_name + display_name = "Service Account for malware scanner cloud run service" + depends_on = [module.apis] +} + +resource "google_project_iam_member" "malware_scanner_iam" { + for_each = toset(["roles/monitoring.metricWriter", "roles/run.invoker", "roles/eventarc.eventReceiver"]) + project = var.projectId + role = each.value + member = "serviceAccount:${google_service_account.malware_scanner_sa.email}" +} + +data "google_storage_project_service_account" "gcs_account" {} + +resource "google_project_iam_binding" "gcs_sa_pubsub_publish" { + project = var.projectId + role = "roles/pubsub.publisher" + members = ["serviceAccount:${data.google_storage_project_service_account.gcs_account.email_address}"] +} + +resource "google_service_account" "build_service_account" { + account_id = "${var.service_name}-build" + display_name = "Service Account for malware scanner cloud run service" + depends_on = [module.apis] +} + +resource "google_project_iam_binding" "build_iam" { + for_each = toset(["roles/storage.objectViewer", "roles/logging.logWriter", "roles/artifactregistry.writer"]) + project = var.projectId + role = each.value + members = ["serviceAccount:${google_service_account.build_service_account.email}"] +} + + +# Create demo buckets if specified +module "demo_buckets" { + source = "./demo_buckets" + count = var.create_demo_buckets ? 1 : 0 + projectId = var.projectId + location = var.bucket_location + service_account_email = google_service_account.malware_scanner_sa.email + uniform_bucket_level_access = var.uniform_bucket_level_access + depends_on = [module.apis] +} + +resource "google_artifact_registry_repository" "repo" { + location = var.region + repository_id = var.service_name + description = "Image registry for Malware Scanner" + format = "DOCKER" + depends_on = [module.apis] +} + +resource "google_storage_bucket" "cvd_mirror_bucket" { + name = var.cvd_mirror_bucket + location = var.bucket_location + uniform_bucket_level_access = var.uniform_bucket_level_access + depends_on = [module.apis] +} + +resource "google_storage_bucket_iam_binding" "cvd_mirror_bucket_sa_binding" { + bucket = google_storage_bucket.cvd_mirror_bucket.name + role = "roles/storage.admin" + members = [ + "serviceAccount:${google_service_account.malware_scanner_sa.email}", + ] +} + +# perform an update/initial load of mirror bucket +resource "null_resource" "populate_cvd_mirror" { + provisioner "local-exec" { + command = join(" ; ", [ + "echo '\n\nPopulating CVD Mirror bucket\n\n'", + "python3 -m venv pyenv", + ". pyenv/bin/activate", + "pip3 install crcmod cvdupdate", + "./updateCvdMirror.sh '${var.cvd_mirror_bucket}'" + ]) + interpreter = ["/bin/bash", "-x", "-e"] + working_dir = local.src_root + } + depends_on = [google_storage_bucket.cvd_mirror_bucket] +} diff --git a/terraform/infra/variables.tf b/terraform/infra/variables.tf new file mode 100644 index 00000000..8aabd729 --- /dev/null +++ b/terraform/infra/variables.tf @@ -0,0 +1,52 @@ +# Copyright 2024 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 +# +# https://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 "projectId" { +} + +variable "bucket_location" { + description = "Location to create Cloud Storage buckets" + default = "US" +} + +variable "region" { + description = "Region to create regional resources" + default = "us-central1" +} + +variable "service_name" { + default = "malware-scanner" +} + +variable "enable_apis" { + description = "Automatically enable required APIs (requires that cloudresourcemanager.googleapis.com and serviceusage.googleapis.com are already enabled)" + default = true + type = bool +} + +variable "create_demo_buckets" { + description = "Create unscanned-PROJECT_ID, clean-PROJECT_ID, and quarantined-PROJECT_ID buckets for demo purposes. " + default = false + type = bool +} + +variable "cvd_mirror_bucket" { + description = "Name of the GCS bucket used for storing a mirror of the clamav CVD database (for example: cvd-mirror-$$PROJECT_ID)" +} + +variable "uniform_bucket_level_access" { + description = "When creating cloud storage buckets, the parameter uniform_bucket_level_access is set to this value" + default = true + type = bool +} diff --git a/terraform/infra/versions.tf b/terraform/infra/versions.tf new file mode 100644 index 00000000..9e6e9849 --- /dev/null +++ b/terraform/infra/versions.tf @@ -0,0 +1,27 @@ +# Copyright 2024 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 +# +# https://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_providers { + google = { + source = "hashicorp/google" + version = ">= 6.2.0" + } + } + required_version = ">= 1.7" + + provider_meta "google" { + module_name = "cloud-solutions/gcs-malware-scanner-deploy-v3.1" + } +} diff --git a/terraform/service/main.tf b/terraform/service/main.tf new file mode 100644 index 00000000..979aef9f --- /dev/null +++ b/terraform/service/main.tf @@ -0,0 +1,149 @@ +# Copyright 2024 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 +# +# https://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. + +## Read config file +locals { + config_json = jsondecode(var.config_json) + unscanned_bucket_names = local.config_json.buckets[*].unscanned +} + +## Verify service account +# +data "google_service_account" "malware_scanner_sa" { + project = var.projectId + account_id = var.service_name +} + + +## Lookup the hash of the latest image +## +data "google_artifact_registry_docker_image" "scanner-service-image" { + project = var.projectId + location = var.region + repository_id = var.service_name + image_name = "${var.service_name}:latest" +} + +## Deploy the Cloud Run Service +# +resource "google_cloud_run_v2_service" "malware_scanner" { + name = var.service_name + location = var.region + ingress = "INGRESS_TRAFFIC_ALL" + + template { + scaling { + max_instance_count = 5 + min_instance_count = 1 + } + service_account = data.google_service_account.malware_scanner_sa.email + containers { + image = data.google_artifact_registry_docker_image.scanner-service-image.self_link + resources { + limits = { + cpu = "1" + memory = "4Gi" + } + cpu_idle = false # CPU is still allocated outside of requests + startup_cpu_boost = true + } + env { + name = "CONFIG_JSON" + value = var.config_json + } + } + max_instance_request_concurrency = 20 + } + traffic { + type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" + percent = 100 + } +} + +## Verify unscanned bucket(s) exist +# +data "google_storage_bucket" "unscanned-bucket" { + project = var.projectId + for_each = toset(local.unscanned_bucket_names) + name = each.value +} + +## Create EventArc Triggers on unscanned bucket(s) +# +resource "google_eventarc_trigger" "gcs-object-written" { + for_each = toset(local.unscanned_bucket_names) + name = "gcs-trigger-${each.value}" + location = var.region + matching_criteria { + attribute = "type" + value = "google.cloud.storage.object.v1.finalized" + } + matching_criteria { + attribute = "bucket" + value = data.google_storage_bucket.unscanned-bucket[each.value].name + } + + destination { + cloud_run_service { + service = google_cloud_run_v2_service.malware_scanner.name + region = google_cloud_run_v2_service.malware_scanner.location + } + } + + service_account = data.google_service_account.malware_scanner_sa.email +} + +## Update pubsub subscriptions to increase deadlines +# +resource "null_resource" "update-subscription-ack-deadline" { + for_each = toset(local.unscanned_bucket_names) + provisioner "local-exec" { + command = "gcloud pubsub subscriptions update \"${google_eventarc_trigger.gcs-object-written[each.key].transport.pubsub.subscription.name}\" --ack-deadline=120" + } +} + +## Deploy scheduled task to refresh the CVD Mirror + +# To avoid having too many clients use the same time slot, +# ClamAV requires that updates are scheduled at a random minute between 3 +# and 57 avoiding multiples of 10. +resource "random_shuffle" "minutes" { + input = [ + "3", "4", "5", "6", "7", "8", "9", + "11", "12", "13", "14", "15", "16", "17", "18", "19", + "21", "22", "23", "24", "25", "26", "27", "28", "29", + "31", "32", "33", "34", "35", "36", "37", "38", "39", + "41", "42", "43", "44", "45", "46", "47", "48", "49", + "51", "52", "53", "54", "55", "56", "57" + ] + result_count = 1 +} + +resource "google_cloud_scheduler_job" "cvd_mirror_update" { + name = "${var.service_name}-cvd-mirror-update" + schedule = "${random_shuffle.minutes.result[0]} */2 * * *" + attempt_deadline = "320s" + region = var.region + http_target { + http_method = "POST" + uri = google_cloud_run_v2_service.malware_scanner.uri + body = base64encode("{\"kind\":\"schedule#cvd_update\"}") + headers = { + "Content-Type" = "application/json" + } + oidc_token { + service_account_email = data.google_service_account.malware_scanner_sa.email + } + } +} diff --git a/terraform/service/variables.tf b/terraform/service/variables.tf new file mode 100644 index 00000000..3ff11e30 --- /dev/null +++ b/terraform/service/variables.tf @@ -0,0 +1,30 @@ +# Copyright 2024 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 +# +# https://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 "projectId" { +} + +variable "region" { + description = "Region to create regional resources" + default = "us-central1" +} + +variable "service_name" { + default = "malware-scanner" +} + +variable "config_json" { + description = "String containing JSON encoded configuration to pass to the cloud run service as environment variable CONFIG_JSON" + type = string +} diff --git a/terraform/service/versions.tf b/terraform/service/versions.tf new file mode 100644 index 00000000..cd15867f --- /dev/null +++ b/terraform/service/versions.tf @@ -0,0 +1,31 @@ +# Copyright 2024 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 +# +# https://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_providers { + google = { + source = "hashicorp/google" + version = ">= 6.2.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.6.3" + } + } + required_version = ">= 1.7" + + provider_meta "google" { + module_name = "cloud-solutions/gcs-malware-scanner-deploy-v3.1" + } +}