Repository: bridgecrewio/checkov

Issue:
Add support for GCP Terraform resources in Image Referencer
**Describe the issue**
Currently we support AWS and Azure resources and it would great to also add GCP ones.



Discussion:

Resolving patch:
diff --git a/checkov/terraform/image_referencer/manager.py b/checkov/terraform/image_referencer/manager.py
--- a/checkov/terraform/image_referencer/manager.py
+++ b/checkov/terraform/image_referencer/manager.py
@@ -4,6 +4,7 @@
 
 from checkov.terraform.image_referencer.provider.aws import AwsTerraformProvider
 from checkov.terraform.image_referencer.provider.azure import AzureTerraformProvider
+from checkov.terraform.image_referencer.provider.gcp import GcpTerraformProvider
 
 if TYPE_CHECKING:
     from checkov.common.images.image_referencer import Image
@@ -21,8 +22,10 @@ def extract_images_from_resources(self) -> list[Image]:
 
         aws_provider = AwsTerraformProvider(graph_connector=self.graph_connector)
         azure_provider = AzureTerraformProvider(graph_connector=self.graph_connector)
+        gcp_provider = GcpTerraformProvider(graph_connector=self.graph_connector)
 
         images.extend(aws_provider.extract_images_from_resources())
         images.extend(azure_provider.extract_images_from_resources())
+        images.extend(gcp_provider.extract_images_from_resources())
 
         return images
diff --git a/checkov/terraform/image_referencer/provider/gcp.py b/checkov/terraform/image_referencer/provider/gcp.py
new file mode 100644
--- /dev/null
+++ b/checkov/terraform/image_referencer/provider/gcp.py
@@ -0,0 +1,69 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any
+
+from checkov.common.util.data_structures_utils import find_in_dict
+from checkov.terraform.image_referencer.base_provider import BaseTerraformProvider
+
+if TYPE_CHECKING:
+    from networkx import DiGraph
+
+
+class GcpTerraformProvider(BaseTerraformProvider):
+    def __init__(self, graph_connector: DiGraph) -> None:
+        super().__init__(
+            graph_connector=graph_connector,
+            supported_resource_types=SUPPORTED_GCP_IMAGE_RESOURCE_TYPES,
+        )
+
+
+def extract_images_from_google_cloudbuild_trigger(resource: dict[str, Any]) -> list[str]:
+    image_names: list[str] = []
+
+    steps = find_in_dict(input_dict=resource, key_path="build/step")
+    if isinstance(steps, list):
+        for definition in steps:
+            name = definition.get("name")
+            if name and isinstance(name, str):
+                image_names.append(name)
+
+    return image_names
+
+
+def extract_images_from_google_cloud_run_service(resource: dict[str, Any]) -> list[str]:
+    image_names: list[str] = []
+
+    name = find_in_dict(input_dict=resource, key_path="template/spec/containers/image")
+    if name and isinstance(name, str):
+        image_names.append(name)
+
+    return image_names
+
+
+def extract_images_from_google_cloud_run_v2_job(resource: dict[str, Any]) -> list[str]:
+    image_names: list[str] = []
+
+    name = find_in_dict(input_dict=resource, key_path="template/template/containers/image")
+    if name and isinstance(name, str):
+        image_names.append(name)
+
+    return image_names
+
+
+def extract_images_from_google_cloud_run_v2_service(resource: dict[str, Any]) -> list[str]:
+    image_names: list[str] = []
+
+    name = find_in_dict(input_dict=resource, key_path="template/containers/image")
+    if name and isinstance(name, str):
+        image_names.append(name)
+
+    return image_names
+
+
+# needs to be at the bottom to add the defined functions
+SUPPORTED_GCP_IMAGE_RESOURCE_TYPES = {
+    "google_cloudbuild_trigger": extract_images_from_google_cloudbuild_trigger,
+    "google_cloud_run_service": extract_images_from_google_cloud_run_service,
+    "google_cloud_run_v2_job": extract_images_from_google_cloud_run_v2_job,
+    "google_cloud_run_v2_service": extract_images_from_google_cloud_run_v2_service,
+}


Test patch:
diff --git a/tests/terraform/image_referencer/provider/test_gcp.py b/tests/terraform/image_referencer/provider/test_gcp.py
new file mode 100644
--- /dev/null
+++ b/tests/terraform/image_referencer/provider/test_gcp.py
@@ -0,0 +1,64 @@
+from networkx import DiGraph
+
+from checkov.common.images.image_referencer import Image
+from checkov.terraform.image_referencer.provider.gcp import GcpTerraformProvider
+
+
+def extract_images_from_resources():
+    # given
+    resource = {
+        "file_path_": "/cloud_run.tf",
+        "__end_line__": 17,
+        "__start_line__": 1,
+        "template": {
+            "spec": {
+                "containers": {
+                    "image": "gcr.io/cloudrun/hello",
+                }
+            }
+        },
+        "resource_type": "google_cloud_run_service",
+    }
+    graph = DiGraph()
+    graph.add_node(1, **resource)
+
+    # when
+    gcp_provider = GcpTerraformProvider(graph_connector=graph)
+    images = gcp_provider.extract_images_from_resources()
+
+    # then
+    assert images == [
+        Image(
+            file_path="/cloud_run.tf",
+            name="gcr.io/cloudrun/hello",
+            start_line=1,
+            end_line=17,
+            related_resource_id="/cloud_run.tf:None",
+        ),
+    ]
+
+
+def test_extract_images_from_resources_with_no_image():
+    # given
+    resource = {
+        "file_path_": "/cloud_run.tf",
+        "__end_line__": 17,
+        "__start_line__": 1,
+        "template": {
+            "spec": {
+                "containers": {
+                    "working_dir": "/tmp",
+                }
+            }
+        },
+        "resource_type": "google_cloud_run_service",
+    }
+    graph = DiGraph()
+    graph.add_node(1, **resource)
+
+    # when
+    gcp_provider = GcpTerraformProvider(graph_connector=graph)
+    images = gcp_provider.extract_images_from_resources()
+
+    # then
+    assert not images
diff --git a/tests/terraform/image_referencer/resources/gcp/cloud_run.tf b/tests/terraform/image_referencer/resources/gcp/cloud_run.tf
new file mode 100644
--- /dev/null
+++ b/tests/terraform/image_referencer/resources/gcp/cloud_run.tf
@@ -0,0 +1,17 @@
+resource "google_cloud_run_service" "example" {
+  name     = "cloudrun-srv"
+  location = "us-central1"
+
+  template {
+    spec {
+      containers {
+        image = "gcr.io/cloudrun/hello"
+      }
+    }
+  }
+
+  traffic {
+    percent         = 100
+    latest_revision = true
+  }
+}
diff --git a/tests/terraform/image_referencer/resources/gcp/cloud_run_v2.tf b/tests/terraform/image_referencer/resources/gcp/cloud_run_v2.tf
new file mode 100644
--- /dev/null
+++ b/tests/terraform/image_referencer/resources/gcp/cloud_run_v2.tf
@@ -0,0 +1,29 @@
+resource "google_cloud_run_v2_job" "example" {
+  name     = "cloudrun-job"
+  location = "us-central1"
+  launch_stage = "BETA"
+
+  template {
+    template {
+      containers {
+        image = "gcr.io/cloudrun/job"
+      }
+    }
+  }
+}
+
+resource "google_cloud_run_v2_service" "example" {
+  name     = "cloudrun-service"
+  location = "us-central1"
+  ingress = "INGRESS_TRAFFIC_ALL"
+
+  binary_authorization {
+    use_default = true
+    breakglass_justification = "Some justification"
+  }
+  template {
+    containers {
+      image = "gcr.io/cloudrun/hello"
+    }
+  }
+}
diff --git a/tests/terraform/image_referencer/resources/gcp/cloudbuild.tf b/tests/terraform/image_referencer/resources/gcp/cloudbuild.tf
new file mode 100644
--- /dev/null
+++ b/tests/terraform/image_referencer/resources/gcp/cloudbuild.tf
@@ -0,0 +1,72 @@
+resource "google_cloudbuild_trigger" "build-trigger" {
+  location = "global"
+
+  trigger_template {
+    branch_name = "main"
+    repo_name   = "my-repo"
+  }
+
+  build {
+    step {
+      name = "gcr.io/cloud-builders/gsutil"
+      args = ["cp", "gs://mybucket/remotefile.zip", "localfile.zip"]
+      timeout = "120s"
+      secret_env = ["MY_SECRET"]
+    }
+
+    step {
+      name   = "ubuntu"
+      script = "echo hello" # using script field
+    }
+
+    source {
+      storage_source {
+        bucket = "mybucket"
+        object = "source_code.tar.gz"
+      }
+    }
+    tags = ["build", "newFeature"]
+    substitutions = {
+      _FOO = "bar"
+      _BAZ = "qux"
+    }
+    queue_ttl = "20s"
+    logs_bucket = "gs://mybucket/logs"
+    secret {
+      kms_key_name = "projects/myProject/locations/global/keyRings/keyring-name/cryptoKeys/key-name"
+      secret_env = {
+        PASSWORD = "ZW5jcnlwdGVkLXBhc3N3b3JkCg=="
+      }
+    }
+    available_secrets {
+      secret_manager {
+        env          = "MY_SECRET"
+        version_name = "projects/myProject/secrets/mySecret/versions/latest"
+      }
+    }
+    artifacts {
+      images = ["gcr.io/$PROJECT_ID/$REPO_NAME:$COMMIT_SHA"]
+      objects {
+        location = "gs://bucket/path/to/somewhere/"
+        paths = ["path"]
+      }
+    }
+    options {
+      source_provenance_hash = ["MD5"]
+      requested_verify_option = "VERIFIED"
+      machine_type = "N1_HIGHCPU_8"
+      disk_size_gb = 100
+      substitution_option = "ALLOW_LOOSE"
+      dynamic_substitutions = true
+      log_streaming_option = "STREAM_OFF"
+      worker_pool = "pool"
+      logging = "LEGACY"
+      env = ["ekey = evalue"]
+      secret_env = ["secretenv = svalue"]
+      volumes {
+        name = "v1"
+        path = "v1"
+      }
+    }
+  }
+}
diff --git a/tests/terraform/image_referencer/test_manager.py b/tests/terraform/image_referencer/test_manager.py
--- a/tests/terraform/image_referencer/test_manager.py
+++ b/tests/terraform/image_referencer/test_manager.py
@@ -37,15 +37,42 @@ def test_extract_images_from_resources():
         },
         "resource_type": "azurerm_batch_pool",
     }
+    gcp_resource = {
+        "file_path_": "/cloud_run.tf",
+        "__end_line__": 17,
+        "__start_line__": 1,
+        "template": {
+            "spec": {
+                "containers": {
+                    "image": "gcr.io/cloudrun/hello",
+                }
+            }
+        },
+        "resource_type": "google_cloud_run_service",
+    }
     graph = DiGraph()
     graph.add_node(1, **aws_resource)
     graph.add_node(2, **azure_resource)
+    graph.add_node(3, **gcp_resource)
 
     # when
     images = TerraformImageReferencerManager(graph_connector=graph).extract_images_from_resources()
 
     # then
     assert images == [
-        Image(file_path="/ecs.tf", name="nginx", start_line=1, end_line=31, related_resource_id='/ecs.tf:None'),
-        Image(file_path="/batch.tf", name="python:3.9-alpine", start_line=1, end_line=25, related_resource_id='/batch.tf:None'),
+        Image(file_path="/ecs.tf", name="nginx", start_line=1, end_line=31, related_resource_id="/ecs.tf:None"),
+        Image(
+            file_path="/batch.tf",
+            name="python:3.9-alpine",
+            start_line=1,
+            end_line=25,
+            related_resource_id="/batch.tf:None",
+        ),
+        Image(
+            file_path="/cloud_run.tf",
+            name="gcr.io/cloudrun/hello",
+            start_line=1,
+            end_line=17,
+            related_resource_id="/cloud_run.tf:None",
+        ),
     ]
diff --git a/tests/terraform/image_referencer/test_runner_gcp_resources.py b/tests/terraform/image_referencer/test_runner_gcp_resources.py
new file mode 100644
--- /dev/null
+++ b/tests/terraform/image_referencer/test_runner_gcp_resources.py
@@ -0,0 +1,157 @@
+from pathlib import Path
+
+from pytest_mock import MockerFixture
+
+from checkov.common.bridgecrew.bc_source import get_source_type
+from checkov.common.output.report import CheckType
+from checkov.runner_filter import RunnerFilter
+from checkov.terraform.runner import Runner
+
+RESOURCES_PATH = Path(__file__).parent / "resources/gcp"
+
+
+def test_cloud_run_resources(mocker: MockerFixture, image_cached_result, license_statuses_result):
+    from checkov.common.bridgecrew.platform_integration import bc_integration
+
+    # given
+    file_name = "cloud_run.tf"
+    image_name = "gcr.io/cloudrun/hello"
+    code_lines = "1-17"
+    test_file = RESOURCES_PATH / file_name
+    runner_filter = RunnerFilter(run_image_referencer=True)
+    bc_integration.bc_source = get_source_type("disabled")
+
+    mocker.patch(
+        "checkov.common.images.image_referencer.image_scanner.get_scan_results_from_cache",
+        return_value=image_cached_result,
+    )
+    mocker.patch(
+        "checkov.common.images.image_referencer.get_license_statuses",
+        return_value=license_statuses_result,
+    )
+
+    # when
+    reports = Runner().run(root_folder="", files=[str(test_file)], runner_filter=runner_filter)
+
+    # then
+    assert len(reports) == 2
+
+    tf_report = next(report for report in reports if report.check_type == CheckType.TERRAFORM)
+    sca_image_report = next(report for report in reports if report.check_type == CheckType.SCA_IMAGE)
+
+    assert len(tf_report.resources) == 1
+    assert len(tf_report.passed_checks) == 0
+    assert len(tf_report.failed_checks) == 0
+    assert len(tf_report.skipped_checks) == 0
+    assert len(tf_report.parsing_errors) == 0
+
+    assert len(sca_image_report.resources) == 3
+    assert sca_image_report.resources == {
+        f"{file_name} ({image_name} lines:{code_lines} (sha256:f9b91f78b0)).musl",
+        f"{file_name} ({image_name} lines:{code_lines} (sha256:f9b91f78b0)).openssl",
+        f"{file_name} ({image_name} lines:{code_lines} (sha256:f9b91f78b0)).zlib",
+    }
+    assert sca_image_report.image_cached_results[0]["dockerImageName"] == "gcr.io/cloudrun/hello"
+    assert (
+        "terraform/image_referencer/resources/gcp/cloud_run.tf:google_cloud_run_service.example"
+        in sca_image_report.image_cached_results[0]["relatedResourceId"]
+    )
+    assert sca_image_report.image_cached_results[0]["packages"] == [
+        {"type": "os", "name": "zlib", "version": "1.2.12-r1", "licenses": ["Zlib"]}
+    ]
+
+    assert len(sca_image_report.passed_checks) == 1
+    assert len(sca_image_report.failed_checks) == 2
+    assert len(sca_image_report.image_cached_results) == 1
+    assert len(sca_image_report.skipped_checks) == 0
+    assert len(sca_image_report.parsing_errors) == 0
+
+
+def test_cloud_run_v2_resources(mocker: MockerFixture, image_cached_result):
+    # given
+    file_name = "cloud_run_v2.tf"
+    image_name_1 = "gcr.io/cloudrun/job"
+    image_name_2 = "gcr.io/cloudrun/hello"
+    code_lines_1 = "1-13"
+    code_lines_2 = "15-29"
+    test_file = RESOURCES_PATH / file_name
+    runner_filter = RunnerFilter(run_image_referencer=True)
+
+    mocker.patch(
+        "checkov.common.images.image_referencer.image_scanner.get_scan_results_from_cache",
+        return_value=image_cached_result,
+    )
+    mocker.patch(
+        "checkov.common.images.image_referencer.get_license_statuses",
+        return_value=[],
+    )
+
+    # when
+    reports = Runner().run(root_folder="", files=[str(test_file)], runner_filter=runner_filter)
+
+    # then
+    assert len(reports) == 2
+
+    tf_report = next(report for report in reports if report.check_type == CheckType.TERRAFORM)
+    sca_image_report = next(report for report in reports if report.check_type == CheckType.SCA_IMAGE)
+
+    assert len(tf_report.resources) == 2
+    assert len(tf_report.passed_checks) == 0
+    assert len(tf_report.failed_checks) == 0
+    assert len(tf_report.skipped_checks) == 0
+    assert len(tf_report.parsing_errors) == 0
+
+    assert len(sca_image_report.resources) == 2
+    assert sca_image_report.resources == {
+        f"{file_name} ({image_name_1} lines:{code_lines_1} (sha256:f9b91f78b0)).zlib",
+        f"{file_name} ({image_name_2} lines:{code_lines_2} (sha256:f9b91f78b0)).zlib",
+    }
+    assert len(sca_image_report.passed_checks) == 0
+    assert len(sca_image_report.failed_checks) == 2
+    assert len(sca_image_report.skipped_checks) == 0
+    assert len(sca_image_report.parsing_errors) == 0
+
+
+def test_cloudbuild_resources(mocker: MockerFixture, image_cached_result):
+    # given
+    file_name = "cloudbuild.tf"
+    image_name_1 = "gcr.io/cloud-builders/gsutil"
+    image_name_2 = "ubuntu"
+    code_lines_1 = "1-72"
+    code_lines_2 = "1-72"
+    test_file = RESOURCES_PATH / file_name
+    runner_filter = RunnerFilter(run_image_referencer=True)
+
+    mocker.patch(
+        "checkov.common.images.image_referencer.image_scanner.get_scan_results_from_cache",
+        return_value=image_cached_result,
+    )
+    mocker.patch(
+        "checkov.common.images.image_referencer.get_license_statuses",
+        return_value=[],
+    )
+
+    # when
+    reports = Runner().run(root_folder="", files=[str(test_file)], runner_filter=runner_filter)
+
+    # then
+    assert len(reports) == 2
+
+    tf_report = next(report for report in reports if report.check_type == CheckType.TERRAFORM)
+    sca_image_report = next(report for report in reports if report.check_type == CheckType.SCA_IMAGE)
+
+    assert len(tf_report.resources) == 1
+    assert len(tf_report.passed_checks) == 0
+    assert len(tf_report.failed_checks) == 0
+    assert len(tf_report.skipped_checks) == 0
+    assert len(tf_report.parsing_errors) == 0
+
+    assert len(sca_image_report.resources) == 2
+    assert sca_image_report.resources == {
+        f"{file_name} ({image_name_1} lines:{code_lines_1} (sha256:f9b91f78b0)).zlib",
+        f"{file_name} ({image_name_2} lines:{code_lines_2} (sha256:f9b91f78b0)).zlib",
+    }
+    assert len(sca_image_report.passed_checks) == 0
+    assert len(sca_image_report.failed_checks) == 2
+    assert len(sca_image_report.skipped_checks) == 0
+    assert len(sca_image_report.parsing_errors) == 0