Skip to content
5 changes: 4 additions & 1 deletion example_config
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
"pubsub_subs": {
"fetch": true
},
"cloud_billing_account": {
"fetch": true
},
"cloud_functions": {
"fetch": true
},
Expand Down Expand Up @@ -99,4 +102,4 @@
"registered_domains": {
"fetch": true
}
}
}
2 changes: 2 additions & 0 deletions src/gcp_scanner/client/client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -43,6 +44,7 @@ class ClientFactory:
"appengine": AppEngineClient,
"bigquery": BQClient,
"bigtableadmin": BigTableClient,
"cloudbilling": CloudBillingClient,
"cloudfunctions": CloudFunctionsClient,
"cloudkms": CloudKMSClient,
"cloudresourcemanager": CloudResourceManagerClient,
Expand Down
38 changes: 38 additions & 0 deletions src/gcp_scanner/client/cloud_billing_client.py
Original file line number Diff line number Diff line change
@@ -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,
)
51 changes: 51 additions & 0 deletions src/gcp_scanner/crawler/cloud_billing_account_crawler.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions src/gcp_scanner/crawler/crawler_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/gcp_scanner/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions src/gcp_scanner/test_acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -48,7 +48,7 @@
CLOUD_FUNCTIONS = 1
ENDPOINTS_COUNT = 0
KMS_COUNT = 1
SERVICES_COUNT = 42
SERVICES_COUNT = 43
SERVICE_ACCOUNTS_COUNT = 2


Expand Down
28 changes: 28 additions & 0 deletions src/gcp_scanner/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
14 changes: 14 additions & 0 deletions test/bootstrap/cloud_billing.sh
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions test/cloud_billing_account
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"name": "projects/test-gcp-scanner-2/billingInfo",
"projectId": "test-gcp-scanner-2",
"billingAccountName": "billingAccounts/0145DD-EDAD5A-6F2373",
"billingEnabled": true
}
]