feat(gcp): add service account impersonation (#4291)

This commit is contained in:
Sergio Garcia
2024-06-26 09:31:47 -04:00
committed by GitHub
parent fc23eccc7b
commit cf84875355
6 changed files with 76 additions and 17 deletions
+10
View File
@@ -25,6 +25,16 @@ Prowler will follow the same credentials search as [Google authentication librar
Those credentials must be associated to a user or service account with proper permissions to do all checks. To make sure, add the `Viewer` role to the member associated with the credentials.
## Impersonate Service Account
If you want to impersonate a GCP service account, you can use the `--impersonate-service-account` argument:
```console
prowler gcp --impersonate-service-account <service-account-email>
```
This argument will use the default credentials to impersonate the service account provided.
# GCP Service APIs
Prowler will use the Google Cloud APIs to get the information needed to perform the checks. Make sure that the following APIs are enabled in the project:
Generated
+3 -3
View File
@@ -1606,13 +1606,13 @@ uritemplate = ">=3.0.1,<5"
[[package]]
name = "google-auth"
version = "2.29.0"
version = "2.30.0"
description = "Google Authentication Library"
optional = false
python-versions = ">=3.7"
files = [
{file = "google-auth-2.29.0.tar.gz", hash = "sha256:672dff332d073227550ffc7457868ac4218d6c500b155fe6cc17d2b13602c360"},
{file = "google_auth-2.29.0-py2.py3-none-any.whl", hash = "sha256:d452ad095688cd52bae0ad6fafe027f6a6d6f560e810fec20914e17a09526415"},
{file = "google-auth-2.30.0.tar.gz", hash = "sha256:ab630a1320f6720909ad76a7dbdb6841cdf5c66b328d690027e4867bdfb16688"},
{file = "google_auth-2.30.0-py2.py3-none-any.whl", hash = "sha256:8df7da660f62757388b8a7f249df13549b3373f24388cb5d2f1dd91cc18180b5"},
]
[package.dependencies]
+46 -14
View File
@@ -3,7 +3,8 @@ import re
import sys
from colorama import Fore, Style
from google import auth
from google.auth import default, impersonated_credentials
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from googleapiclient import discovery
from googleapiclient.errors import HttpError
@@ -38,9 +39,12 @@ class GcpProvider(Provider):
input_project_ids = arguments.project_id
excluded_project_ids = arguments.excluded_project_id
credentials_file = arguments.credentials_file
self._impersonated_service_account = arguments.impersonate_service_account
list_project_ids = arguments.list_project_id
self._session = self.setup_session(credentials_file)
self._session = self.setup_session(
credentials_file, self._impersonated_service_account
)
self._project_ids = []
self._projects = {}
@@ -120,6 +124,10 @@ class GcpProvider(Provider):
def projects(self):
return self._projects
@property
def impersonated_service_account(self):
return self._impersonated_service_account
@property
def project_ids(self):
return self._project_ids
@@ -168,14 +176,39 @@ class GcpProvider(Provider):
# "partition": "identity.partition",
}
def setup_session(self, credentials_file):
def setup_session(self, credentials_file: str, service_account: str) -> Credentials:
"""
Setup the GCP session with the provided credentials file or service account to impersonate
Args:
credentials_file: str
service_account: str
Returns:
Credentials object
"""
try:
scopes = ["https://www.googleapis.com/auth/cloud-platform"]
if credentials_file:
logger.info(f"Using credentials file: {credentials_file}")
self.__set_gcp_creds_env_var__(credentials_file)
credentials, _ = auth.default(
scopes=["https://www.googleapis.com/auth/cloud-platform"]
)
# Get default credentials
credentials, _ = default(scopes=scopes)
# Refresh the credentials to ensure they are valid
credentials.refresh(Request())
logger.info(f"Initial credentials: {credentials}")
if service_account:
# Create the impersonated credentials
credentials = impersonated_credentials.Credentials(
source_credentials=credentials,
target_principal=service_account,
target_scopes=scopes,
)
logger.info(f"Impersonated credentials: {credentials}")
return credentials
except Exception as error:
logger.critical(
@@ -197,6 +230,10 @@ class GcpProvider(Provider):
f"GCP Account: {Fore.YELLOW}{self.identity.profile}{Style.RESET_ALL}",
f"GCP Project IDs: {Fore.YELLOW}{', '.join(self.project_ids)}{Style.RESET_ALL}",
]
if self.impersonated_service_account:
report_lines.append(
f"Impersonated Service Account: {Fore.YELLOW}{self.impersonated_service_account}{Style.RESET_ALL}"
)
if self.excluded_project_ids:
report_lines.append(
f"Excluded GCP Project IDs: {Fore.YELLOW}{', '.join(self.excluded_project_ids)}{Style.RESET_ALL}"
@@ -259,14 +296,9 @@ class GcpProvider(Provider):
f"{http_error.__class__.__name__}[{http_error.__traceback__.tb_lineno}]: {http_error}"
)
except Exception as error:
if error.__class__.__name__ == "RefreshError":
logger.critical(
"Google Cloud SDK has not been authenticated or the credentials have expired. Authenticate by running 'gcloud auth application-default login' then retry."
)
else:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
sys.exit(1)
finally:
return projects
@@ -12,6 +12,12 @@ def init_parser(self):
metavar="FILE_PATH",
help="Authenticate using a Google Service Account Application Credentials JSON file",
)
gcp_auth_modes_group.add_argument(
"--impersonate-service-account",
nargs="?",
metavar="SERVICE_ACCOUNT",
help="Impersonate a Google Service Account",
)
# Projects
gcp_projects_subparser = gcp_parser.add_argument_group("Projects")
gcp_projects_subparser.add_argument(
+8
View File
@@ -1229,6 +1229,14 @@ class Test_Parser:
assert parsed.provider == "gcp"
assert parsed.list_project_id
def test_parser_gcp_impersonate_service_account(self):
argument = "--impersonate-service-account"
service_account = "test@test.iam.gserviceaccount.com"
command = [prowler_command, "gcp", argument, service_account]
parsed = self.parser.parse(command)
assert parsed.provider == "gcp"
assert parsed.impersonate_service_account == service_account
def test_parser_kubernetes_auth_kubeconfig_file(self):
argument = "--kubeconfig-file"
file = "config"
+3
View File
@@ -20,6 +20,7 @@ class TestGCPProvider:
arguments.excluded_project_id = []
arguments.list_project_id = False
arguments.credentials_file = ""
arguments.impersonate_service_account = ""
arguments.config_file = default_config_file_path
arguments.fixer_config = default_fixer_config_file_path
@@ -56,6 +57,7 @@ class TestGCPProvider:
arguments.excluded_project_id = []
arguments.list_project_id = False
arguments.credentials_file = ""
arguments.impersonate_service_account = ""
arguments.config_file = default_config_file_path
arguments.fixer_config = default_fixer_config_file_path
@@ -128,6 +130,7 @@ class TestGCPProvider:
arguments.excluded_project_id = []
arguments.list_project_id = False
arguments.credentials_file = ""
arguments.impersonate_service_account = ""
arguments.config_file = default_config_file_path
arguments.fixer_config = default_fixer_config_file_path