Repository: google/gcp_scanner (language: python)

Issue:
Epic: add support for additional GCP resources.
📝 **List of resources:**

I've compiled the following resources that we need to add support for. Feel free to suggest any other resources that should be prioritized. Additionally, let me know if there's anything else we should include in the list.

- [x] #250
- [x] #255 
- [x] #260
- [ ] cloudshell
- [ ] cloudasset
- [ ] cloudidentity
- [ ] cloudbilling
- [ ] cloudtrace
- [ ] Compute/securitypolicies
- [ ] Cloud Monitoring dashboard
- [ ] Compute/sslCertificates

🛠️ **Implementation Steps:**

Here's a brief description of how to implement new resource crawling:

**Step 1:**

Browse through the [google-api-python-client](https://googleapis.github.io/google-api-python-client/docs/dyn/) library to find the suitable client and determine the data to crawl.

**Step 2:**

If the client isn't already implemented in the [client package](https://github.com/google/gcp_scanner/tree/main/src/gcp_scanner/client), create a client factory.

**Step 3:**

For your crawler, establish a new crawler factory in the [crawler package](https://github.com/google/gcp_scanner/tree/main/src/gcp_scanner/crawler).

**Step 4:**

In the `scanner.py` file, include the crawler in the crawl loop and update the `crawl_client_map`. Don't forget to modify the `example.config` to account for the new crawler.

**Step 5:**

Ensure proper unit tests are in place. If applicable, add acceptance tests (If you are a team member who has access to the `test_gcp_project` in the Google Cloud Platform)

📝 **Note:** Please bear in mind that the code is undergoing significant refactoring, so the above-mentioned process might change.


Discussion / hints:
Hey @sudiptob2 I am complete beginner  to open source contribution. Can I work on these issues ? 
Hey @Mac16661, we do not assign bugs. If you feel like you want to contribute, you can just submit PR fixing one of the problems.
Hi everyone, I have raised a PR to add support to CloudAsset resource. PR: https://github.com/google/gcp_scanner/pull/305. Please let me know if any changes are required.
Hi Guys, I have raised a PR adding the support to Cloud Trace resource. Let me know in any case of changes ✌️
PR: https://github.com/google/gcp_scanner/pull/307

Fix patch:
diff --git a/example_config b/example_config
index 2bfe6748..52749ea6 100644
--- a/example_config
+++ b/example_config
@@ -55,6 +55,9 @@
   "pubsub_subs": {
     "fetch": true
   },
+  "cloud_billing_account": {
+    "fetch": true
+  },
   "cloud_functions": {
     "fetch": true
   },
@@ -99,4 +102,4 @@
   "registered_domains": {
     "fetch": true
   }
-}
\ No newline at end of file
+}
diff --git a/src/gcp_scanner/client/client_factory.py b/src/gcp_scanner/client/client_factory.py
index ac7e90a6..b0066f13 100644
--- a/src/gcp_scanner/client/client_factory.py
+++ b/src/gcp_scanner/client/client_factory.py
@@ -17,6 +17,7 @@
 from gcp_scanner.client.appengine_client import AppEngineClient
 from gcp_scanner.client.bigquery_client import BQClient
 from gcp_scanner.client.bigtable_client import BigTableClient
+from gcp_scanner.client.cloud_billing_client import CloudBillingClient
 from gcp_scanner.client.cloud_functions_client import CloudFunctionsClient
 from gcp_scanner.client.cloud_resource_manager_client import CloudResourceManagerClient
 from gcp_scanner.client.compute_client import ComputeClient
@@ -43,6 +44,7 @@ class ClientFactory:
     "appengine": AppEngineClient,
     "bigquery": BQClient,
     "bigtableadmin": BigTableClient,
+    "cloudbilling": CloudBillingClient,
     "cloudfunctions": CloudFunctionsClient,
     "cloudkms": CloudKMSClient,
     "cloudresourcemanager": CloudResourceManagerClient,
diff --git a/src/gcp_scanner/client/cloud_billing_client.py b/src/gcp_scanner/client/cloud_billing_client.py
new file mode 100644
index 00000000..d671a5c3
--- /dev/null
+++ b/src/gcp_scanner/client/cloud_billing_client.py
@@ -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
+#
+#       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.
+
+from googleapiclient import discovery
+from httplib2 import Credentials
+
+from .interface_client import IClient
+
+
+class CloudBillingClient(IClient):
+  """CloudBillingClient class."""
+
+  def get_service(self, credentials: Credentials) -> discovery.Resource:
+    """Get discovery service for cloud billing resource.
+
+    Args:
+      credentials: An google.oauth2.credentials.Credentials object.
+
+    Returns:
+      An object of discovery.Resource
+    """
+    return discovery.build(
+      "cloudbilling",
+      "v1",
+      credentials=credentials,
+      cache_discovery=False,
+    )
diff --git a/src/gcp_scanner/crawler/cloud_billing_account_crawler.py b/src/gcp_scanner/crawler/cloud_billing_account_crawler.py
new file mode 100644
index 00000000..991df7a6
--- /dev/null
+++ b/src/gcp_scanner/crawler/cloud_billing_account_crawler.py
@@ -0,0 +1,51 @@
+#  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
+#
+#       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.
+
+import logging
+import sys
+from typing import Dict, Any, Union
+
+from googleapiclient import discovery
+
+from gcp_scanner.crawler.interface_crawler import ICrawler
+
+
+class CloudBillingAccountCrawler(ICrawler):
+  '''Handle crawling of Cloud Billing Account data.'''
+
+  def crawl(self, project_id: str, service: discovery.Resource,
+            config: Dict[str, Union[bool, str]] = None) -> Dict[str, Any]:
+    '''Retrieve the Cloud Billing Account associated with the project.
+
+  Args:
+    project_id: A name of a project to query info about.
+    service: A resource object for interacting with the Cloud Billing API.
+    config: Configuration options for the crawler (Optional).
+
+    Returns:
+      A list of resource objects representing the crawled data.
+    '''
+
+    logging.info("Retrieving CloudBillingAccount")
+    billing_account = list()
+    try:
+      request = service.projects().getBillingInfo(name=f"projects/{project_id}")
+      if request is not None:
+        response = request.execute()
+        billing_account.append(response)
+    except Exception:
+      logging.info("Failed to retrieve CloudBillingAccount for project %s", project_id)
+      logging.info(sys.exc_info())
+
+    return billing_account
diff --git a/src/gcp_scanner/crawler/crawler_factory.py b/src/gcp_scanner/crawler/crawler_factory.py
index e31a4089..ab37a24a 100644
--- a/src/gcp_scanner/crawler/crawler_factory.py
+++ b/src/gcp_scanner/crawler/crawler_factory.py
@@ -17,6 +17,7 @@
 from gcp_scanner.crawler.app_services_crawler import AppServicesCrawler
 from gcp_scanner.crawler.bigquery_crawler import BigQueryCrawler
 from gcp_scanner.crawler.bigtable_instances_crawler import BigTableInstancesCrawler
+from gcp_scanner.crawler.cloud_billing_account_crawler import CloudBillingAccountCrawler
 from gcp_scanner.crawler.cloud_functions_crawler import CloudFunctionsCrawler
 from gcp_scanner.crawler.cloud_resource_manager_iam_policy_crawler import CloudResourceManagerIAMPolicyCrawler
 from gcp_scanner.crawler.cloud_resource_manager_project_info_crawler import CloudResourceManagerProjectInfoCrawler
@@ -50,6 +51,7 @@
   "app_services": AppServicesCrawler,
   "bigtable_instances": BigTableInstancesCrawler,
   "bq": BigQueryCrawler,
+  "cloud_billing_account": CloudBillingAccountCrawler,
   "cloud_functions": CloudFunctionsCrawler,
   "compute_disks": ComputeDisksCrawler,
   "compute_images": ComputeImagesCrawler,
diff --git a/src/gcp_scanner/scanner.py b/src/gcp_scanner/scanner.py
index 878077a7..006c80e1 100644
--- a/src/gcp_scanner/scanner.py
+++ b/src/gcp_scanner/scanner.py
@@ -94,6 +94,7 @@
     'app_services': 'appengine',
     'bigtable_instances': 'bigtableadmin',
     'bq': 'bigquery',
+    'cloud_billing_account': 'cloudbilling',
     'cloud_functions': 'cloudfunctions',
     'compute_disks': 'compute',
     'compute_images': 'compute',


Test patch:
diff --git a/src/gcp_scanner/test_acceptance.py b/src/gcp_scanner/test_acceptance.py
index 8ec40f36..b5be37e9 100644
--- a/src/gcp_scanner/test_acceptance.py
+++ b/src/gcp_scanner/test_acceptance.py
@@ -23,7 +23,7 @@
 
 from . import scanner
 
-RESOURCE_COUNT = 31
+RESOURCE_COUNT = 32
 RESULTS_JSON_COUNT = 1
 PROJECT_INFO_COUNT = 5
 IAM_POLICY_COUNT = 12
@@ -48,7 +48,7 @@
 CLOUD_FUNCTIONS = 1
 ENDPOINTS_COUNT = 0
 KMS_COUNT = 1
-SERVICES_COUNT = 42
+SERVICES_COUNT = 43
 SERVICE_ACCOUNTS_COUNT = 2
 
 
diff --git a/src/gcp_scanner/test_unit.py b/src/gcp_scanner/test_unit.py
index 53069bb3..eb707a67 100644
--- a/src/gcp_scanner/test_unit.py
+++ b/src/gcp_scanner/test_unit.py
@@ -37,6 +37,7 @@
 from .client.bigquery_client import BQClient
 from .client.bigtable_client import BigTableClient
 from .client.client_factory import ClientFactory
+from .client.client_factory import CloudBillingClient
 from .client.cloud_functions_client import CloudFunctionsClient
 from .client.cloud_resource_manager_client import CloudResourceManagerClient
 from .client.compute_client import ComputeClient
@@ -58,6 +59,7 @@
 from .crawler.app_services_crawler import AppServicesCrawler
 from .crawler.bigquery_crawler import BigQueryCrawler
 from .crawler.bigtable_instances_crawler import BigTableInstancesCrawler
+from .crawler.cloud_billing_account_crawler import CloudBillingAccountCrawler
 from .crawler.cloud_functions_crawler import CloudFunctionsCrawler
 from .crawler.cloud_resource_manager_iam_policy_crawler import CloudResourceManagerIAMPolicyCrawler
 from .crawler.cloud_resource_manager_project_info_crawler import CloudResourceManagerProjectInfoCrawler
@@ -628,6 +630,22 @@ def test_pubsub_subs(self):
       )
     )
 
+  def test_cloud_billing_account(self):
+    """Test CloudBillingAccount list"""
+    self.assertTrue(
+      verify(
+        CrawlerFactory.create_crawler(
+          "cloud_billing_account",
+        ).crawl(
+          PROJECT_NAME,
+          ClientFactory.get_client("cloudbilling").get_service(
+            self.credentials,
+          ),
+        ),
+        "cloud_billing_account",
+      )
+    )
+
   def test_cloud_functions(self):
     """Test CloudFunctions list."""
     self.assertTrue(
@@ -894,6 +912,11 @@ def test_get_client_pubsub(self):
     client = ClientFactory.get_client("pubsub")
     self.assertIsInstance(client, PubSubClient)
 
+  def test_get_client_cloudbilling(self):
+    """Test get_client method with 'cloudbilling' name."""
+    client = ClientFactory.get_client("cloudbilling")
+    self.assertIsInstance(client, CloudBillingClient)
+
   def test_get_client_cloudfunctions(self):
     """Test get_client method with 'cloudfunctions' name."""
     client = ClientFactory.get_client("cloudfunctions")
@@ -980,6 +1003,11 @@ def test_create_crawler_bigquery(self):
     crawler = CrawlerFactory.create_crawler("bq")
     self.assertIsInstance(crawler, BigQueryCrawler)
 
+  def test_create_crawler_cloud_billing_account(self):
+    """Test create_crawler method with 'cloud_billing_account' name."""
+    crawler = CrawlerFactory.create_crawler("cloud_billing_account")
+    self.assertIsInstance(crawler, CloudBillingAccountCrawler)
+
   def test_create_crawler_cloud_functions(self):
     """Test create_crawler method with 'cloud_functions' name."""
     crawler = CrawlerFactory.create_crawler("cloud_functions")
diff --git a/test/bootstrap/cloud_billing.sh b/test/bootstrap/cloud_billing.sh
new file mode 100644
index 00000000..fbe13979
--- /dev/null
+++ b/test/bootstrap/cloud_billing.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+gcloud services enable cloudbilling.googleapis.com
+
+#### incase no billing account is associated with project, use steps below
+
+# # install alpha component if not installed 
+# gcloud components install alpha
+
+# # list billing accounts associated with the current user, 
+# gcloud alpha billing accounts list
+
+# # link one of the billing accounts with project my-project
+# gcloud alpha billing projects link my-project --billing-account 0X0X0X-0X0X0X-0X0X0X
diff --git a/test/cloud_billing_account b/test/cloud_billing_account
new file mode 100644
index 00000000..38210b5e
--- /dev/null
+++ b/test/cloud_billing_account
@@ -0,0 +1,8 @@
+[
+  {
+    "name": "projects/test-gcp-scanner-2/billingInfo",
+    "projectId": "test-gcp-scanner-2",
+    "billingAccountName": "billingAccounts/0145DD-EDAD5A-6F2373",
+    "billingEnabled": true
+  }
+]