cartography 0.111.0__py3-none-any.whl → 0.112.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.
- cartography/_version.py +2 -2
- cartography/cli.py +11 -0
- cartography/config.py +8 -0
- cartography/data/indexes.cypher +0 -2
- cartography/intel/aws/apigateway.py +126 -17
- cartography/intel/aws/ec2/instances.py +3 -1
- cartography/intel/aws/ec2/network_interfaces.py +1 -1
- cartography/intel/aws/ec2/vpc_peerings.py +262 -125
- cartography/intel/azure/__init__.py +35 -32
- cartography/intel/azure/subscription.py +2 -2
- cartography/intel/azure/tenant.py +39 -30
- cartography/intel/azure/util/credentials.py +49 -174
- cartography/intel/entra/__init__.py +47 -1
- cartography/intel/entra/applications.py +220 -170
- cartography/intel/entra/groups.py +41 -22
- cartography/intel/entra/ou.py +28 -20
- cartography/intel/entra/users.py +24 -18
- cartography/intel/gcp/__init__.py +25 -8
- cartography/intel/gcp/compute.py +47 -12
- cartography/intel/kubernetes/__init__.py +26 -0
- cartography/intel/kubernetes/eks.py +402 -0
- cartography/intel/kubernetes/rbac.py +133 -0
- cartography/models/aws/apigateway/apigatewayintegration.py +79 -0
- cartography/models/aws/apigateway/apigatewaymethod.py +74 -0
- cartography/models/aws/ec2/vpc_peering.py +157 -0
- cartography/models/azure/principal.py +44 -0
- cartography/models/azure/tenant.py +20 -0
- cartography/models/kubernetes/clusterrolebindings.py +40 -0
- cartography/models/kubernetes/groups.py +107 -0
- cartography/models/kubernetes/oidc.py +51 -0
- cartography/models/kubernetes/rolebindings.py +40 -0
- cartography/models/kubernetes/users.py +105 -0
- cartography/util.py +2 -0
- {cartography-0.111.0.dist-info → cartography-0.112.0.dist-info}/METADATA +8 -5
- {cartography-0.111.0.dist-info → cartography-0.112.0.dist-info}/RECORD +39 -31
- cartography/data/jobs/cleanup/aws_import_vpc_peering_cleanup.json +0 -45
- {cartography-0.111.0.dist-info → cartography-0.112.0.dist-info}/WHEEL +0 -0
- {cartography-0.111.0.dist-info → cartography-0.112.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.111.0.dist-info → cartography-0.112.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.111.0.dist-info → cartography-0.112.0.dist-info}/top_level.txt +0 -0
cartography/intel/entra/ou.py
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
) ->
|
|
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
|
-
|
|
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
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
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)
|
cartography/intel/entra/users.py
CHANGED
|
@@ -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) ->
|
|
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
|
-
|
|
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]) ->
|
|
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
|
-
|
|
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
|
-
#
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
174
|
+
"iam",
|
|
175
|
+
"v1",
|
|
176
|
+
http=_authorized_http_with_timeout(credentials),
|
|
177
|
+
cache_discovery=False,
|
|
161
178
|
)
|
|
162
179
|
|
|
163
180
|
|
cartography/intel/gcp/compute.py
CHANGED
|
@@ -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
|
-
|
|
124
|
-
|
|
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) ->
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
+
import boto3
|
|
3
4
|
from neo4j import Session
|
|
4
5
|
|
|
5
6
|
from cartography.config import Config
|
|
6
7
|
from cartography.intel.kubernetes.clusters import sync_kubernetes_cluster
|
|
8
|
+
from cartography.intel.kubernetes.eks import sync as sync_eks
|
|
7
9
|
from cartography.intel.kubernetes.namespaces import sync_namespaces
|
|
8
10
|
from cartography.intel.kubernetes.pods import sync_pods
|
|
9
11
|
from cartography.intel.kubernetes.rbac import sync_kubernetes_rbac
|
|
@@ -15,6 +17,17 @@ from cartography.util import timeit
|
|
|
15
17
|
logger = logging.getLogger(__name__)
|
|
16
18
|
|
|
17
19
|
|
|
20
|
+
def get_region_from_arn(arn: str) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Extract AWS region from EKS cluster ARN.
|
|
23
|
+
Example: arn:aws:eks:us-east-1:205930638578:cluster/infra-test-eks → us-east-1
|
|
24
|
+
"""
|
|
25
|
+
parts = arn.split(":")
|
|
26
|
+
if len(parts) < 6 or parts[2] != "eks":
|
|
27
|
+
raise ValueError(f"Invalid EKS cluster ARN: {arn}")
|
|
28
|
+
return parts[3]
|
|
29
|
+
|
|
30
|
+
|
|
18
31
|
@timeit
|
|
19
32
|
def start_k8s_ingestion(session: Session, config: Config) -> None:
|
|
20
33
|
if not config.update_tag:
|
|
@@ -42,6 +55,19 @@ def start_k8s_ingestion(session: Session, config: Config) -> None:
|
|
|
42
55
|
sync_kubernetes_rbac(
|
|
43
56
|
session, client, config.update_tag, common_job_parameters
|
|
44
57
|
)
|
|
58
|
+
if config.managed_kubernetes == "eks":
|
|
59
|
+
# EKS identity provider sync
|
|
60
|
+
boto3_session = boto3.Session()
|
|
61
|
+
region = get_region_from_arn(cluster_info.get("id", ""))
|
|
62
|
+
sync_eks(
|
|
63
|
+
session,
|
|
64
|
+
client,
|
|
65
|
+
boto3_session,
|
|
66
|
+
region,
|
|
67
|
+
config.update_tag,
|
|
68
|
+
cluster_info.get("id", ""),
|
|
69
|
+
cluster_info.get("name", ""),
|
|
70
|
+
)
|
|
45
71
|
all_pods = sync_pods(
|
|
46
72
|
session,
|
|
47
73
|
client,
|