cartography 0.111.0rc1__py3-none-any.whl → 0.113.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cartography might be problematic. Click here for more details.

Files changed (81) hide show
  1. cartography/_version.py +2 -2
  2. cartography/cli.py +57 -0
  3. cartography/config.py +24 -0
  4. cartography/data/indexes.cypher +0 -6
  5. cartography/data/jobs/analysis/keycloak_inheritance.json +30 -0
  6. cartography/intel/aws/apigateway.py +128 -17
  7. cartography/intel/aws/apigatewayv2.py +116 -0
  8. cartography/intel/aws/ec2/instances.py +3 -1
  9. cartography/intel/aws/ec2/network_interfaces.py +1 -1
  10. cartography/intel/aws/ec2/vpc_peerings.py +262 -125
  11. cartography/intel/aws/resources.py +2 -0
  12. cartography/intel/azure/__init__.py +35 -32
  13. cartography/intel/azure/subscription.py +2 -2
  14. cartography/intel/azure/tenant.py +39 -30
  15. cartography/intel/azure/util/credentials.py +49 -174
  16. cartography/intel/entra/__init__.py +47 -1
  17. cartography/intel/entra/applications.py +220 -170
  18. cartography/intel/entra/groups.py +41 -22
  19. cartography/intel/entra/ou.py +28 -20
  20. cartography/intel/entra/users.py +24 -18
  21. cartography/intel/gcp/__init__.py +32 -11
  22. cartography/intel/gcp/compute.py +47 -12
  23. cartography/intel/gcp/dns.py +82 -169
  24. cartography/intel/gcp/iam.py +66 -54
  25. cartography/intel/gcp/storage.py +75 -159
  26. cartography/intel/github/repos.py +19 -10
  27. cartography/intel/github/util.py +12 -0
  28. cartography/intel/keycloak/__init__.py +153 -0
  29. cartography/intel/keycloak/authenticationexecutions.py +322 -0
  30. cartography/intel/keycloak/authenticationflows.py +77 -0
  31. cartography/intel/keycloak/clients.py +187 -0
  32. cartography/intel/keycloak/groups.py +126 -0
  33. cartography/intel/keycloak/identityproviders.py +94 -0
  34. cartography/intel/keycloak/organizations.py +163 -0
  35. cartography/intel/keycloak/realms.py +61 -0
  36. cartography/intel/keycloak/roles.py +202 -0
  37. cartography/intel/keycloak/scopes.py +73 -0
  38. cartography/intel/keycloak/users.py +70 -0
  39. cartography/intel/keycloak/util.py +47 -0
  40. cartography/intel/kubernetes/__init__.py +26 -0
  41. cartography/intel/kubernetes/eks.py +402 -0
  42. cartography/intel/kubernetes/rbac.py +133 -0
  43. cartography/models/aws/apigateway/apigatewayintegration.py +79 -0
  44. cartography/models/aws/apigateway/apigatewaymethod.py +74 -0
  45. cartography/models/aws/apigatewayv2/__init__.py +0 -0
  46. cartography/models/aws/apigatewayv2/apigatewayv2.py +53 -0
  47. cartography/models/aws/ec2/vpc_peering.py +157 -0
  48. cartography/models/azure/principal.py +44 -0
  49. cartography/models/azure/tenant.py +20 -0
  50. cartography/models/gcp/dns.py +109 -0
  51. cartography/models/gcp/iam.py +3 -0
  52. cartography/models/gcp/storage/__init__.py +0 -0
  53. cartography/models/gcp/storage/bucket.py +119 -0
  54. cartography/models/keycloak/__init__.py +0 -0
  55. cartography/models/keycloak/authenticationexecution.py +160 -0
  56. cartography/models/keycloak/authenticationflow.py +54 -0
  57. cartography/models/keycloak/client.py +177 -0
  58. cartography/models/keycloak/group.py +101 -0
  59. cartography/models/keycloak/identityprovider.py +89 -0
  60. cartography/models/keycloak/organization.py +116 -0
  61. cartography/models/keycloak/organizationdomain.py +73 -0
  62. cartography/models/keycloak/realm.py +173 -0
  63. cartography/models/keycloak/role.py +126 -0
  64. cartography/models/keycloak/scope.py +73 -0
  65. cartography/models/keycloak/user.py +51 -0
  66. cartography/models/kubernetes/clusterrolebindings.py +40 -0
  67. cartography/models/kubernetes/groups.py +107 -0
  68. cartography/models/kubernetes/oidc.py +51 -0
  69. cartography/models/kubernetes/rolebindings.py +40 -0
  70. cartography/models/kubernetes/users.py +105 -0
  71. cartography/sync.py +2 -0
  72. cartography/util.py +10 -0
  73. {cartography-0.111.0rc1.dist-info → cartography-0.113.0.dist-info}/METADATA +9 -5
  74. {cartography-0.111.0rc1.dist-info → cartography-0.113.0.dist-info}/RECORD +78 -41
  75. cartography/data/jobs/cleanup/aws_import_vpc_peering_cleanup.json +0 -45
  76. cartography/data/jobs/cleanup/gcp_dns_cleanup.json +0 -29
  77. cartography/data/jobs/cleanup/gcp_storage_bucket_cleanup.json +0 -29
  78. {cartography-0.111.0rc1.dist-info → cartography-0.113.0.dist-info}/WHEEL +0 -0
  79. {cartography-0.111.0rc1.dist-info → cartography-0.113.0.dist-info}/entry_points.txt +0 -0
  80. {cartography-0.111.0rc1.dist-info → cartography-0.113.0.dist-info}/licenses/LICENSE +0 -0
  81. {cartography-0.111.0rc1.dist-info → cartography-0.113.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,8 @@
1
1
  # cartography/intel/entra/ou.py
2
2
  import logging
3
3
  from typing import Any
4
+ from typing import AsyncGenerator
5
+ from typing import Generator
4
6
 
5
7
  import neo4j
6
8
  from azure.identity import ClientSecretCredential
@@ -9,7 +11,6 @@ from msgraph.generated.models.administrative_unit import AdministrativeUnit
9
11
 
10
12
  from cartography.client.core.tx import load
11
13
  from cartography.graph.job import GraphJob
12
- from cartography.intel.entra.users import load_tenant
13
14
  from cartography.models.entra.ou import EntraOUSchema
14
15
  from cartography.util import timeit
15
16
 
@@ -17,12 +18,12 @@ logger = logging.getLogger(__name__)
17
18
 
18
19
 
19
20
  @timeit
20
- async def get_entra_ous(client: GraphServiceClient) -> list[AdministrativeUnit]:
21
+ async def get_entra_ous(
22
+ client: GraphServiceClient,
23
+ ) -> AsyncGenerator[AdministrativeUnit, None]:
21
24
  """
22
- Get all OUs from Microsoft Graph API with pagination support
25
+ Get all OUs from Microsoft Graph API with pagination support using a generator
23
26
  """
24
- all_units: list[AdministrativeUnit] = []
25
-
26
27
  # Initialize first page request
27
28
  current_request = client.directory.administrative_units
28
29
 
@@ -30,7 +31,8 @@ async def get_entra_ous(client: GraphServiceClient) -> list[AdministrativeUnit]:
30
31
  try:
31
32
  response = await current_request.get()
32
33
  if response and response.value:
33
- all_units.extend(response.value)
34
+ for unit in response.value:
35
+ yield unit
34
36
 
35
37
  # Handle next page using OData link
36
38
  if response.odata_next_link:
@@ -45,18 +47,15 @@ async def get_entra_ous(client: GraphServiceClient) -> list[AdministrativeUnit]:
45
47
  logger.error(f"Failed to retrieve administrative units: {str(e)}")
46
48
  current_request = None
47
49
 
48
- return all_units
49
-
50
50
 
51
51
  def transform_ous(
52
52
  units: list[AdministrativeUnit], tenant_id: str
53
- ) -> list[dict[str, Any]]:
53
+ ) -> Generator[dict[str, Any], None, None]:
54
54
  """
55
- Transform the API response into the format expected by our schema
55
+ Transform the API response into the format expected by our schema using a generator
56
56
  """
57
- result: list[dict[str, Any]] = []
58
57
  for unit in units:
59
- transformed_unit = {
58
+ yield {
60
59
  "id": unit.id,
61
60
  "display_name": unit.display_name,
62
61
  "description": unit.description,
@@ -66,8 +65,6 @@ def transform_ous(
66
65
  "deleted_date_time": unit.deleted_date_time,
67
66
  "tenant_id": tenant_id,
68
67
  }
69
- result.append(transformed_unit)
70
- return result
71
68
 
72
69
 
73
70
  @timeit
@@ -116,13 +113,24 @@ async def sync_entra_ous(
116
113
  credential, scopes=["https://graph.microsoft.com/.default"]
117
114
  )
118
115
 
119
- # Get OUs
120
- units = await get_entra_ous(client)
121
- transformed_units = transform_ous(units, tenant_id)
116
+ # Process OUs in batches
117
+ batch_size = 100 # OUs are typically fewer than users/groups
118
+ units_batch = []
119
+
120
+ async for unit in get_entra_ous(client):
121
+ units_batch.append(unit)
122
+
123
+ if len(units_batch) >= batch_size:
124
+ transformed_units = list(transform_ous(units_batch, tenant_id))
125
+ load_ous(
126
+ neo4j_session, transformed_units, update_tag, common_job_parameters
127
+ )
128
+ units_batch.clear()
122
129
 
123
- # Load data
124
- load_tenant(neo4j_session, {"id": tenant_id}, update_tag)
125
- load_ous(neo4j_session, transformed_units, update_tag, common_job_parameters)
130
+ # Process any remaining OUs
131
+ if units_batch:
132
+ transformed_units = list(transform_ous(units_batch, tenant_id))
133
+ load_ous(neo4j_session, transformed_units, update_tag, common_job_parameters)
126
134
 
127
135
  # Cleanup stale data
128
136
  cleanup_ous(neo4j_session, common_job_parameters)
@@ -1,5 +1,7 @@
1
1
  import logging
2
2
  from typing import Any
3
+ from typing import AsyncGenerator
4
+ from typing import Generator
3
5
 
4
6
  import neo4j
5
7
  from azure.identity import ClientSecretCredential
@@ -71,7 +73,7 @@ async def get_tenant(client: GraphServiceClient) -> Organization:
71
73
 
72
74
 
73
75
  @timeit
74
- async def get_users(client: GraphServiceClient) -> list[User]:
76
+ async def get_users(client: GraphServiceClient) -> AsyncGenerator[User, None]:
75
77
  """Fetch all users with their manager reference in as few requests as possible.
76
78
 
77
79
  We leverage `$expand=manager($select=id)` so the manager's *id* is hydrated
@@ -80,7 +82,6 @@ async def get_users(client: GraphServiceClient) -> list[User]:
80
82
  when a user has no manager assigned.
81
83
  """
82
84
 
83
- all_users: list[User] = []
84
85
  request_configuration = client.users.UsersRequestBuilderGetRequestConfiguration(
85
86
  query_parameters=client.users.UsersRequestBuilderGetQueryParameters(
86
87
  top=999,
@@ -91,7 +92,9 @@ async def get_users(client: GraphServiceClient) -> list[User]:
91
92
 
92
93
  page = await client.users.get(request_configuration=request_configuration)
93
94
  while page:
94
- all_users.extend(page.value)
95
+ if page.value:
96
+ for user in page.value:
97
+ yield user
95
98
  if not page.odata_next_link:
96
99
  break
97
100
 
@@ -104,23 +107,20 @@ async def get_users(client: GraphServiceClient) -> list[User]:
104
107
  )
105
108
  break
106
109
 
107
- return all_users
108
-
109
110
 
110
111
  @timeit
111
112
  # The manager reference is now embedded in the user objects courtesy of the
112
113
  # `$expand` we added above, so we no longer need a separate `manager_map`.
113
- def transform_users(users: list[User]) -> list[dict[str, Any]]:
114
+ def transform_users(users: list[User]) -> Generator[dict[str, Any], None, None]:
114
115
  """Convert MS Graph SDK `User` models into dicts matching our schema."""
115
116
 
116
- result: list[dict[str, Any]] = []
117
117
  for user in users:
118
118
  manager_id: str | None = None
119
119
  if getattr(user, "manager", None) is not None:
120
120
  # The SDK materialises `manager` as a DirectoryObject (or subclass)
121
121
  manager_id = getattr(user.manager, "id", None)
122
122
 
123
- transformed_user = {
123
+ yield {
124
124
  "id": user.id,
125
125
  "user_principal_name": user.user_principal_name,
126
126
  "display_name": user.display_name,
@@ -143,9 +143,6 @@ def transform_users(users: list[User]) -> list[dict[str, Any]]:
143
143
  "age_group": user.age_group,
144
144
  "manager_id": manager_id,
145
145
  }
146
- result.append(transformed_user)
147
-
148
- return result
149
146
 
150
147
 
151
148
  @timeit
@@ -240,14 +237,23 @@ async def sync_entra_users(
240
237
  credential, scopes=["https://graph.microsoft.com/.default"]
241
238
  )
242
239
 
243
- # Fetch tenant and users (with manager reference already populated by `$expand`)
244
- tenant = await get_tenant(client)
245
- users = await get_users(client)
240
+ # Process users in batches to reduce memory consumption
241
+ batch_size = (
242
+ 500 # Process users in larger batches since they're simpler than groups
243
+ )
244
+ users_batch = []
245
+
246
+ async for user in get_users(client):
247
+ users_batch.append(user)
246
248
 
247
- transformed_users = transform_users(users)
248
- transformed_tenant = transform_tenant(tenant, tenant_id)
249
+ if len(users_batch) >= batch_size:
250
+ transformed_users = list(transform_users(users_batch))
251
+ load_users(neo4j_session, transformed_users, tenant_id, update_tag)
252
+ users_batch.clear()
249
253
 
250
- load_tenant(neo4j_session, transformed_tenant, update_tag)
251
- load_users(neo4j_session, transformed_users, tenant_id, update_tag)
254
+ # Process any remaining users
255
+ if users_batch:
256
+ transformed_users = list(transform_users(users_batch))
257
+ load_users(neo4j_session, transformed_users, tenant_id, update_tag)
252
258
 
253
259
  cleanup(neo4j_session, common_job_parameters)
@@ -7,10 +7,12 @@ from typing import Optional
7
7
  from typing import Set
8
8
 
9
9
  import googleapiclient.discovery
10
+ import httplib2
10
11
  import neo4j
11
12
  from google.auth import default
12
13
  from google.auth.credentials import Credentials as GoogleCredentials
13
14
  from google.auth.exceptions import DefaultCredentialsError
15
+ from google_auth_httplib2 import AuthorizedHttp
14
16
  from googleapiclient.discovery import Resource
15
17
 
16
18
  from cartography.config import Config
@@ -39,6 +41,18 @@ service_names = Services(
39
41
  iam="iam.googleapis.com",
40
42
  )
41
43
 
44
+ # Default HTTP timeout (seconds) for Google API clients built via discovery.build
45
+ _GCP_HTTP_TIMEOUT = 120
46
+
47
+
48
+ def _authorized_http_with_timeout(
49
+ credentials: GoogleCredentials, timeout: int = _GCP_HTTP_TIMEOUT
50
+ ) -> AuthorizedHttp:
51
+ """
52
+ Build an AuthorizedHttp with a per-request timeout, avoiding global socket timeouts.
53
+ """
54
+ return AuthorizedHttp(credentials, http=httplib2.Http(timeout=timeout))
55
+
42
56
 
43
57
  def _get_crm_resource_v1(credentials: GoogleCredentials) -> Resource:
44
58
  """
@@ -52,7 +66,7 @@ def _get_crm_resource_v1(credentials: GoogleCredentials) -> Resource:
52
66
  return googleapiclient.discovery.build(
53
67
  "cloudresourcemanager",
54
68
  "v1",
55
- credentials=credentials,
69
+ http=_authorized_http_with_timeout(credentials),
56
70
  cache_discovery=False,
57
71
  )
58
72
 
@@ -67,7 +81,7 @@ def _get_crm_resource_v2(credentials: GoogleCredentials) -> Resource:
67
81
  return googleapiclient.discovery.build(
68
82
  "cloudresourcemanager",
69
83
  "v2",
70
- credentials=credentials,
84
+ http=_authorized_http_with_timeout(credentials),
71
85
  cache_discovery=False,
72
86
  )
73
87
 
@@ -82,7 +96,7 @@ def _get_compute_resource(credentials: GoogleCredentials) -> Resource:
82
96
  return googleapiclient.discovery.build(
83
97
  "compute",
84
98
  "v1",
85
- credentials=credentials,
99
+ http=_authorized_http_with_timeout(credentials),
86
100
  cache_discovery=False,
87
101
  )
88
102
 
@@ -99,7 +113,7 @@ def _get_storage_resource(credentials: GoogleCredentials) -> Resource:
99
113
  return googleapiclient.discovery.build(
100
114
  "storage",
101
115
  "v1",
102
- credentials=credentials,
116
+ http=_authorized_http_with_timeout(credentials),
103
117
  cache_discovery=False,
104
118
  )
105
119
 
@@ -115,7 +129,7 @@ def _get_container_resource(credentials: GoogleCredentials) -> Resource:
115
129
  return googleapiclient.discovery.build(
116
130
  "container",
117
131
  "v1",
118
- credentials=credentials,
132
+ http=_authorized_http_with_timeout(credentials),
119
133
  cache_discovery=False,
120
134
  )
121
135
 
@@ -131,7 +145,7 @@ def _get_dns_resource(credentials: GoogleCredentials) -> Resource:
131
145
  return googleapiclient.discovery.build(
132
146
  "dns",
133
147
  "v1",
134
- credentials=credentials,
148
+ http=_authorized_http_with_timeout(credentials),
135
149
  cache_discovery=False,
136
150
  )
137
151
 
@@ -147,7 +161,7 @@ def _get_serviceusage_resource(credentials: GoogleCredentials) -> Resource:
147
161
  return googleapiclient.discovery.build(
148
162
  "serviceusage",
149
163
  "v1",
150
- credentials=credentials,
164
+ http=_authorized_http_with_timeout(credentials),
151
165
  cache_discovery=False,
152
166
  )
153
167
 
@@ -157,7 +171,10 @@ def _get_iam_resource(credentials: GoogleCredentials) -> Resource:
157
171
  Instantiates a Google IAM resource object to call the IAM API.
158
172
  """
159
173
  return googleapiclient.discovery.build(
160
- "iam", "v1", credentials=credentials, cache_discovery=False
174
+ "iam",
175
+ "v1",
176
+ http=_authorized_http_with_timeout(credentials),
177
+ cache_discovery=False,
161
178
  )
162
179
 
163
180
 
@@ -463,9 +480,13 @@ def get_gcp_credentials() -> Optional[GoogleCredentials]:
463
480
  :return: GoogleCredentials
464
481
  """
465
482
  try:
466
- # Explicitly use Application Default Credentials.
467
- # See https://google-auth.readthedocs.io/en/master/user-guide.html#application-default-credentials
468
- credentials, project_id = default()
483
+ # Explicitly use Application Default Credentials with the cloud-platform scope.
484
+ # Some versions of google-auth/google-api-python-client require explicit scopes for service accounts;
485
+ # without this, token refresh may fail with `invalid_scope`.
486
+ # See: https://cloud.google.com/docs/authentication#calling
487
+ credentials, project_id = default(
488
+ scopes=["https://www.googleapis.com/auth/cloud-platform"],
489
+ )
469
490
  return credentials
470
491
  except DefaultCredentialsError as e:
471
492
  logger.debug(
@@ -11,8 +11,8 @@ from typing import Optional
11
11
  from typing import Set
12
12
 
13
13
  import neo4j
14
- from googleapiclient.discovery import HttpError
15
14
  from googleapiclient.discovery import Resource
15
+ from googleapiclient.errors import HttpError
16
16
 
17
17
  from cartography.client.core.tx import load
18
18
  from cartography.graph.job import GraphJob
@@ -24,6 +24,10 @@ logger = logging.getLogger(__name__)
24
24
  InstanceUriPrefix = namedtuple("InstanceUriPrefix", "zone_name project_id")
25
25
 
26
26
 
27
+ # Maximum number of retries for Google API requests
28
+ GOOGLE_API_NUM_RETRIES = 5
29
+
30
+
27
31
  def _get_error_reason(http_error: HttpError) -> str:
28
32
  """
29
33
  Helper function to get an error reason out of the googleapiclient's HttpError object
@@ -66,7 +70,7 @@ def get_zones_in_project(
66
70
  """
67
71
  try:
68
72
  req = compute.zones().list(project=project_id, maxResults=max_results)
69
- res = req.execute()
73
+ res = req.execute(num_retries=GOOGLE_API_NUM_RETRIES)
70
74
  return res["items"]
71
75
  except HttpError as e:
72
76
  reason = _get_error_reason(e)
@@ -120,22 +124,53 @@ def get_gcp_instance_responses(
120
124
  response_objects: List[Resource] = []
121
125
  for zone in zones:
122
126
  req = compute.instances().list(project=project_id, zone=zone["name"])
123
- res = req.execute()
124
- response_objects.append(res)
127
+ try:
128
+ res = req.execute(num_retries=GOOGLE_API_NUM_RETRIES)
129
+ response_objects.append(res)
130
+ except HttpError as e:
131
+ reason = _get_error_reason(e)
132
+ if reason in {"backendError", "rateLimitExceeded", "internalError"}:
133
+ logger.warning(
134
+ "Transient error listing instances for project %s zone %s: %s; skipping this zone.",
135
+ project_id,
136
+ zone.get("name"),
137
+ e,
138
+ )
139
+ continue
140
+ raise
125
141
  return response_objects
126
142
 
127
143
 
128
144
  @timeit
129
- def get_gcp_subnets(projectid: str, region: str, compute: Resource) -> Resource:
145
+ def get_gcp_subnets(projectid: str, region: str, compute: Resource) -> Dict:
130
146
  """
131
- Return list of all subnets in the given projectid and region
132
- :param projectid: THe projectid
147
+ Return list of all subnets in the given projectid and region. If the API
148
+ call times out mid-pagination, return any subnets gathered so far rather than
149
+ bubbling the error up to the caller.
150
+ :param projectid: The project ID
133
151
  :param region: The region to pull subnets from
134
152
  :param compute: The compute resource object created by googleapiclient.discovery.build()
135
153
  :return: Response object containing data on all GCP subnets for a given project
136
154
  """
137
155
  req = compute.subnetworks().list(project=projectid, region=region)
138
- return req.execute()
156
+ items: List[Dict] = []
157
+ response_id = f"projects/{projectid}/regions/{region}/subnetworks"
158
+ while req is not None:
159
+ try:
160
+ res = req.execute(num_retries=GOOGLE_API_NUM_RETRIES)
161
+ except TimeoutError:
162
+ logger.warning(
163
+ "GCP: subnetworks.list for project %s region %s timed out; continuing with partial data.",
164
+ projectid,
165
+ region,
166
+ )
167
+ break
168
+ items.extend(res.get("items", []))
169
+ response_id = res.get("id", response_id)
170
+ req = compute.subnetworks().list_next(
171
+ previous_request=req, previous_response=res
172
+ )
173
+ return {"id": response_id, "items": items}
139
174
 
140
175
 
141
176
  @timeit
@@ -147,7 +182,7 @@ def get_gcp_vpcs(projectid: str, compute: Resource) -> Resource:
147
182
  :return: VPC response object
148
183
  """
149
184
  req = compute.networks().list(project=projectid)
150
- return req.execute()
185
+ return req.execute(num_retries=GOOGLE_API_NUM_RETRIES)
151
186
 
152
187
 
153
188
  @timeit
@@ -164,7 +199,7 @@ def get_gcp_regional_forwarding_rules(
164
199
  :return: Response object containing data on all GCP forwarding rules for a given project
165
200
  """
166
201
  req = compute.forwardingRules().list(project=project_id, region=region)
167
- return req.execute()
202
+ return req.execute(num_retries=GOOGLE_API_NUM_RETRIES)
168
203
 
169
204
 
170
205
  @timeit
@@ -176,7 +211,7 @@ def get_gcp_global_forwarding_rules(project_id: str, compute: Resource) -> Resou
176
211
  :return: Response object containing data on all GCP forwarding rules for a given project
177
212
  """
178
213
  req = compute.globalForwardingRules().list(project=project_id)
179
- return req.execute()
214
+ return req.execute(num_retries=GOOGLE_API_NUM_RETRIES)
180
215
 
181
216
 
182
217
  @timeit
@@ -188,7 +223,7 @@ def get_gcp_firewall_ingress_rules(project_id: str, compute: Resource) -> Resour
188
223
  :return: Firewall response object
189
224
  """
190
225
  req = compute.firewalls().list(project=project_id, filter='(direction="INGRESS")')
191
- return req.execute()
226
+ return req.execute(num_retries=GOOGLE_API_NUM_RETRIES)
192
227
 
193
228
 
194
229
  @timeit