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.
- cartography/_version.py +2 -2
- cartography/cli.py +57 -0
- cartography/config.py +24 -0
- cartography/data/indexes.cypher +0 -6
- cartography/data/jobs/analysis/keycloak_inheritance.json +30 -0
- cartography/intel/aws/apigateway.py +128 -17
- cartography/intel/aws/apigatewayv2.py +116 -0
- 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/aws/resources.py +2 -0
- 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 +32 -11
- cartography/intel/gcp/compute.py +47 -12
- cartography/intel/gcp/dns.py +82 -169
- cartography/intel/gcp/iam.py +66 -54
- cartography/intel/gcp/storage.py +75 -159
- cartography/intel/github/repos.py +19 -10
- cartography/intel/github/util.py +12 -0
- cartography/intel/keycloak/__init__.py +153 -0
- cartography/intel/keycloak/authenticationexecutions.py +322 -0
- cartography/intel/keycloak/authenticationflows.py +77 -0
- cartography/intel/keycloak/clients.py +187 -0
- cartography/intel/keycloak/groups.py +126 -0
- cartography/intel/keycloak/identityproviders.py +94 -0
- cartography/intel/keycloak/organizations.py +163 -0
- cartography/intel/keycloak/realms.py +61 -0
- cartography/intel/keycloak/roles.py +202 -0
- cartography/intel/keycloak/scopes.py +73 -0
- cartography/intel/keycloak/users.py +70 -0
- cartography/intel/keycloak/util.py +47 -0
- 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/apigatewayv2/__init__.py +0 -0
- cartography/models/aws/apigatewayv2/apigatewayv2.py +53 -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/gcp/dns.py +109 -0
- cartography/models/gcp/iam.py +3 -0
- cartography/models/gcp/storage/__init__.py +0 -0
- cartography/models/gcp/storage/bucket.py +119 -0
- cartography/models/keycloak/__init__.py +0 -0
- cartography/models/keycloak/authenticationexecution.py +160 -0
- cartography/models/keycloak/authenticationflow.py +54 -0
- cartography/models/keycloak/client.py +177 -0
- cartography/models/keycloak/group.py +101 -0
- cartography/models/keycloak/identityprovider.py +89 -0
- cartography/models/keycloak/organization.py +116 -0
- cartography/models/keycloak/organizationdomain.py +73 -0
- cartography/models/keycloak/realm.py +173 -0
- cartography/models/keycloak/role.py +126 -0
- cartography/models/keycloak/scope.py +73 -0
- cartography/models/keycloak/user.py +51 -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/sync.py +2 -0
- cartography/util.py +10 -0
- {cartography-0.111.0rc1.dist-info → cartography-0.113.0.dist-info}/METADATA +9 -5
- {cartography-0.111.0rc1.dist-info → cartography-0.113.0.dist-info}/RECORD +78 -41
- cartography/data/jobs/cleanup/aws_import_vpc_peering_cleanup.json +0 -45
- cartography/data/jobs/cleanup/gcp_dns_cleanup.json +0 -29
- cartography/data/jobs/cleanup/gcp_storage_bucket_cleanup.json +0 -29
- {cartography-0.111.0rc1.dist-info → cartography-0.113.0.dist-info}/WHEEL +0 -0
- {cartography-0.111.0rc1.dist-info → cartography-0.113.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.111.0rc1.dist-info → cartography-0.113.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.111.0rc1.dist-info → cartography-0.113.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from typing import Generator
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
# Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
|
|
7
|
+
_TIMEOUT = (60, 60)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_paginated(
|
|
11
|
+
api_session: requests.Session,
|
|
12
|
+
endpoint: str,
|
|
13
|
+
items_per_page: int = 100,
|
|
14
|
+
params: dict[str, Any] | None = None,
|
|
15
|
+
) -> Generator[dict[str, Any], None, None]:
|
|
16
|
+
"""Fetch paginated results from a REST API endpoint.
|
|
17
|
+
|
|
18
|
+
This function handles pagination by making multiple requests to the API
|
|
19
|
+
until all pages of results have been retrieved.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
api_session (requests.Session): The requests session to use for making API calls.
|
|
23
|
+
endpoint (str): The API endpoint to fetch data from.
|
|
24
|
+
items_per_page (int, optional): The number of items to retrieve per page. Defaults to 100.
|
|
25
|
+
params (dict[str, Any] | None, optional): Additional query parameters to include in the request. Defaults to None.
|
|
26
|
+
|
|
27
|
+
Yields:
|
|
28
|
+
Generator[dict[str, Any], None, None]: A generator that yields the individual items from the paginated response.
|
|
29
|
+
"""
|
|
30
|
+
has_more = True
|
|
31
|
+
offset = 0
|
|
32
|
+
while has_more:
|
|
33
|
+
if params is None:
|
|
34
|
+
payload = {}
|
|
35
|
+
else:
|
|
36
|
+
payload = params.copy()
|
|
37
|
+
payload["first"] = offset
|
|
38
|
+
payload["max"] = items_per_page
|
|
39
|
+
req = api_session.get(endpoint, params=payload, timeout=_TIMEOUT)
|
|
40
|
+
req.raise_for_status()
|
|
41
|
+
data = req.json()
|
|
42
|
+
if not data:
|
|
43
|
+
break
|
|
44
|
+
yield from data
|
|
45
|
+
if len(data) < items_per_page:
|
|
46
|
+
has_more = False
|
|
47
|
+
offset += len(data)
|
|
@@ -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,
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
import boto3
|
|
7
|
+
import neo4j
|
|
8
|
+
import yaml
|
|
9
|
+
from kubernetes.client.models import V1ConfigMap
|
|
10
|
+
|
|
11
|
+
from cartography.client.core.tx import load
|
|
12
|
+
from cartography.graph.job import GraphJob
|
|
13
|
+
from cartography.intel.kubernetes.util import K8sClient
|
|
14
|
+
from cartography.models.kubernetes.groups import KubernetesGroupSchema
|
|
15
|
+
from cartography.models.kubernetes.oidc import KubernetesOIDCProviderSchema
|
|
16
|
+
from cartography.models.kubernetes.users import KubernetesUserSchema
|
|
17
|
+
from cartography.util import aws_handle_regions
|
|
18
|
+
from cartography.util import timeit
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@timeit
|
|
24
|
+
def get_aws_auth_configmap(client: K8sClient) -> V1ConfigMap:
|
|
25
|
+
"""
|
|
26
|
+
Get aws-auth ConfigMap from kube-system namespace.
|
|
27
|
+
"""
|
|
28
|
+
logger.info(f"Retrieving aws-auth ConfigMap from cluster {client.name}")
|
|
29
|
+
return client.core.read_namespaced_config_map(
|
|
30
|
+
name="aws-auth", namespace="kube-system"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_aws_auth_map(configmap: V1ConfigMap) -> Dict[str, List[Dict[str, Any]]]:
|
|
35
|
+
"""
|
|
36
|
+
Parse mapRoles and mapUsers from aws-auth ConfigMap.
|
|
37
|
+
|
|
38
|
+
:param configmap: V1ConfigMap containing aws-auth data
|
|
39
|
+
:return: Dictionary with 'roles' and 'users' keys containing their respective mappings
|
|
40
|
+
"""
|
|
41
|
+
result: Dict[str, List[Dict[str, Any]]] = {"roles": [], "users": []}
|
|
42
|
+
|
|
43
|
+
# Parse mapRoles
|
|
44
|
+
if "mapRoles" in configmap.data:
|
|
45
|
+
map_roles_yaml = configmap.data["mapRoles"]
|
|
46
|
+
role_mappings = yaml.safe_load(map_roles_yaml) or []
|
|
47
|
+
|
|
48
|
+
# Filter out templated entries for now (https://github.com/cartography-cncf/cartography/issues/1854)
|
|
49
|
+
filtered_role_mappings = []
|
|
50
|
+
for mapping in role_mappings:
|
|
51
|
+
username = mapping.get("username", "")
|
|
52
|
+
if "{{" in username:
|
|
53
|
+
logger.debug(f"Skipping templated username in mapRoles: {username}")
|
|
54
|
+
continue
|
|
55
|
+
filtered_role_mappings.append(mapping)
|
|
56
|
+
|
|
57
|
+
result["roles"] = filtered_role_mappings
|
|
58
|
+
logger.info(
|
|
59
|
+
f"Parsed {len(filtered_role_mappings)} role mappings from aws-auth ConfigMap"
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
logger.info("No mapRoles found in aws-auth ConfigMap")
|
|
63
|
+
|
|
64
|
+
# Parse mapUsers
|
|
65
|
+
if "mapUsers" in configmap.data:
|
|
66
|
+
map_users_yaml = configmap.data["mapUsers"]
|
|
67
|
+
user_mappings = yaml.safe_load(map_users_yaml) or []
|
|
68
|
+
|
|
69
|
+
filtered_user_mappings = []
|
|
70
|
+
for mapping in user_mappings:
|
|
71
|
+
username = mapping.get("username", "")
|
|
72
|
+
if "{{" in username:
|
|
73
|
+
logger.debug(f"Skipping templated username in mapUsers: {username}")
|
|
74
|
+
continue
|
|
75
|
+
filtered_user_mappings.append(mapping)
|
|
76
|
+
|
|
77
|
+
result["users"] = filtered_user_mappings
|
|
78
|
+
logger.info(
|
|
79
|
+
f"Parsed {len(filtered_user_mappings)} user mappings from aws-auth ConfigMap"
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
logger.info("No mapUsers found in aws-auth ConfigMap")
|
|
83
|
+
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def transform_aws_auth_mappings(
|
|
88
|
+
auth_mappings: Dict[str, List[Dict[str, Any]]], cluster_name: str
|
|
89
|
+
) -> Dict[str, List[Dict[str, Any]]]:
|
|
90
|
+
"""
|
|
91
|
+
Transform both role and user mappings from aws-auth ConfigMap into combined user/group data.
|
|
92
|
+
"""
|
|
93
|
+
all_users = []
|
|
94
|
+
all_groups = []
|
|
95
|
+
|
|
96
|
+
# Process role mappings if they exist
|
|
97
|
+
if auth_mappings.get("roles"):
|
|
98
|
+
for mapping in auth_mappings.get("roles", []):
|
|
99
|
+
role_arn = mapping.get("rolearn")
|
|
100
|
+
username = mapping.get("username")
|
|
101
|
+
group_names = mapping.get("groups", [])
|
|
102
|
+
|
|
103
|
+
if not role_arn:
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
# Create user data with AWS role relationship (only if username is provided)
|
|
107
|
+
if username:
|
|
108
|
+
all_users.append(
|
|
109
|
+
{
|
|
110
|
+
"id": f"{cluster_name}/{username}",
|
|
111
|
+
"name": username,
|
|
112
|
+
"cluster_name": cluster_name,
|
|
113
|
+
"aws_role_arn": role_arn,
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Create group data with AWS role relationship for each group
|
|
118
|
+
for group_name in group_names:
|
|
119
|
+
all_groups.append(
|
|
120
|
+
{
|
|
121
|
+
"id": f"{cluster_name}/{group_name}",
|
|
122
|
+
"name": group_name,
|
|
123
|
+
"cluster_name": cluster_name,
|
|
124
|
+
"aws_role_arn": role_arn,
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Process user mappings if they exist
|
|
129
|
+
if auth_mappings.get("users"):
|
|
130
|
+
for mapping in auth_mappings.get("users", []):
|
|
131
|
+
user_arn = mapping.get("userarn")
|
|
132
|
+
username = mapping.get("username")
|
|
133
|
+
group_names = mapping.get("groups", [])
|
|
134
|
+
|
|
135
|
+
if not user_arn:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
# Create user data with AWS user relationship (only if username is provided)
|
|
139
|
+
if username:
|
|
140
|
+
all_users.append(
|
|
141
|
+
{
|
|
142
|
+
"id": f"{cluster_name}/{username}",
|
|
143
|
+
"name": username,
|
|
144
|
+
"cluster_name": cluster_name,
|
|
145
|
+
"aws_user_arn": user_arn,
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Create group data with AWS user relationship for each group
|
|
150
|
+
for group_name in group_names:
|
|
151
|
+
all_groups.append(
|
|
152
|
+
{
|
|
153
|
+
"id": f"{cluster_name}/{group_name}",
|
|
154
|
+
"name": group_name,
|
|
155
|
+
"cluster_name": cluster_name,
|
|
156
|
+
"aws_user_arn": user_arn,
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Count entries with vs without usernames for visibility
|
|
161
|
+
role_entries_with_username = sum(
|
|
162
|
+
1 for mapping in auth_mappings.get("roles", []) if mapping.get("username")
|
|
163
|
+
)
|
|
164
|
+
user_entries_with_username = sum(
|
|
165
|
+
1 for mapping in auth_mappings.get("users", []) if mapping.get("username")
|
|
166
|
+
)
|
|
167
|
+
total_entries_with_username = (
|
|
168
|
+
role_entries_with_username + user_entries_with_username
|
|
169
|
+
)
|
|
170
|
+
total_entries = len(auth_mappings.get("roles", [])) + len(
|
|
171
|
+
auth_mappings.get("users", [])
|
|
172
|
+
)
|
|
173
|
+
entries_without_username = total_entries - total_entries_with_username
|
|
174
|
+
|
|
175
|
+
logger.info(
|
|
176
|
+
f"Transformed {len(all_users)} users (from {total_entries_with_username} entries with usernames) "
|
|
177
|
+
f"and {len(all_groups)} groups from {len(auth_mappings.get('roles', []))} role mappings "
|
|
178
|
+
f"and {len(auth_mappings.get('users', []))} user mappings "
|
|
179
|
+
f"({entries_without_username} entries without usernames created groups only)"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return {"users": all_users, "groups": all_groups}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@timeit
|
|
186
|
+
@aws_handle_regions
|
|
187
|
+
def get_oidc_provider(
|
|
188
|
+
boto3_session: boto3.session.Session,
|
|
189
|
+
region: str,
|
|
190
|
+
cluster_name: str,
|
|
191
|
+
) -> List[Dict[str, Any]]:
|
|
192
|
+
"""
|
|
193
|
+
Get external OIDC identity provider configurations for an EKS cluster.
|
|
194
|
+
|
|
195
|
+
Returns raw AWS API responses for configured external identity providers.
|
|
196
|
+
"""
|
|
197
|
+
client = boto3_session.client("eks", region_name=region)
|
|
198
|
+
oidc_providers = []
|
|
199
|
+
|
|
200
|
+
# Extract just the cluster name from ARN if needed
|
|
201
|
+
# ARN format: arn:aws:eks:region:account:cluster/cluster-name
|
|
202
|
+
if cluster_name.startswith("arn:aws:eks:"):
|
|
203
|
+
cluster_name = cluster_name.split("/")[-1]
|
|
204
|
+
|
|
205
|
+
# Get configured external identity provider configs
|
|
206
|
+
configs_response = client.list_identity_provider_configs(clusterName=cluster_name)
|
|
207
|
+
|
|
208
|
+
for config in configs_response["identityProviderConfigs"]:
|
|
209
|
+
if config["type"] == "oidc":
|
|
210
|
+
# Get detailed config for this OIDC provider
|
|
211
|
+
detail_response = client.describe_identity_provider_config(
|
|
212
|
+
clusterName=cluster_name,
|
|
213
|
+
identityProviderConfig={"type": "oidc", "name": config["name"]},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
oidc_providers.append(detail_response["identityProviderConfig"]["oidc"])
|
|
217
|
+
|
|
218
|
+
return oidc_providers
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def transform_oidc_provider(
|
|
222
|
+
oidc_providers: List[Dict[str, Any]],
|
|
223
|
+
cluster_name: str,
|
|
224
|
+
) -> List[Dict[str, Any]]:
|
|
225
|
+
"""
|
|
226
|
+
Transform raw AWS OIDC provider data into standardized format.
|
|
227
|
+
|
|
228
|
+
Takes raw AWS API responses and creates OIDC provider nodes that match
|
|
229
|
+
the KubernetesOIDCProvider data model for infrastructure metadata.
|
|
230
|
+
"""
|
|
231
|
+
transformed_providers = []
|
|
232
|
+
|
|
233
|
+
for provider in oidc_providers:
|
|
234
|
+
# Extract fields from raw AWS API response
|
|
235
|
+
provider_name = provider["identityProviderConfigName"]
|
|
236
|
+
issuer_url = provider["issuerUrl"]
|
|
237
|
+
|
|
238
|
+
# Create a unique ID for the external OIDC provider
|
|
239
|
+
# Format: cluster_name/oidc/provider_name
|
|
240
|
+
provider_id = f"{cluster_name}/oidc/{provider_name}"
|
|
241
|
+
|
|
242
|
+
transformed_provider = {
|
|
243
|
+
"id": provider_id,
|
|
244
|
+
"issuer_url": issuer_url,
|
|
245
|
+
"cluster_name": cluster_name,
|
|
246
|
+
"k8s_platform": "eks",
|
|
247
|
+
"client_id": provider.get("clientId", ""),
|
|
248
|
+
"status": provider.get("status", "UNKNOWN"),
|
|
249
|
+
"name": provider_name,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
transformed_providers.append(transformed_provider)
|
|
253
|
+
|
|
254
|
+
return transformed_providers
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def load_oidc_provider(
|
|
258
|
+
neo4j_session: neo4j.Session,
|
|
259
|
+
oidc_providers: List[Dict[str, Any]],
|
|
260
|
+
update_tag: int,
|
|
261
|
+
cluster_id: str,
|
|
262
|
+
cluster_name: str,
|
|
263
|
+
) -> None:
|
|
264
|
+
"""
|
|
265
|
+
Load OIDC providers and their relationships to users and groups into Neo4j.
|
|
266
|
+
"""
|
|
267
|
+
logger.info(f"Loading {len(oidc_providers)} EKS OIDC providers")
|
|
268
|
+
load(
|
|
269
|
+
neo4j_session,
|
|
270
|
+
KubernetesOIDCProviderSchema(),
|
|
271
|
+
oidc_providers,
|
|
272
|
+
lastupdated=update_tag,
|
|
273
|
+
CLUSTER_ID=cluster_id,
|
|
274
|
+
CLUSTER_NAME=cluster_name,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def load_aws_auth_mappings(
|
|
279
|
+
neo4j_session: neo4j.Session,
|
|
280
|
+
users: List[Dict[str, Any]],
|
|
281
|
+
groups: List[Dict[str, Any]],
|
|
282
|
+
update_tag: int,
|
|
283
|
+
cluster_id: str,
|
|
284
|
+
cluster_name: str,
|
|
285
|
+
) -> None:
|
|
286
|
+
"""
|
|
287
|
+
Load Kubernetes Users/Groups with AWS Role and User relationships into Neo4j using schema-based loading.
|
|
288
|
+
"""
|
|
289
|
+
logger.info(f"Loading {len(users)} Kubernetes Users with AWS mappings")
|
|
290
|
+
|
|
291
|
+
# Load Kubernetes Users with AWS relationships
|
|
292
|
+
if users:
|
|
293
|
+
load(
|
|
294
|
+
neo4j_session,
|
|
295
|
+
KubernetesUserSchema(),
|
|
296
|
+
users,
|
|
297
|
+
lastupdated=update_tag,
|
|
298
|
+
CLUSTER_ID=cluster_id,
|
|
299
|
+
CLUSTER_NAME=cluster_name,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
logger.info(f"Loading {len(groups)} Kubernetes Groups with AWS mappings")
|
|
303
|
+
|
|
304
|
+
# Load Kubernetes Groups with AWS relationships
|
|
305
|
+
if groups:
|
|
306
|
+
load(
|
|
307
|
+
neo4j_session,
|
|
308
|
+
KubernetesGroupSchema(),
|
|
309
|
+
groups,
|
|
310
|
+
lastupdated=update_tag,
|
|
311
|
+
CLUSTER_ID=cluster_id,
|
|
312
|
+
CLUSTER_NAME=cluster_name,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def cleanup(
|
|
317
|
+
neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]
|
|
318
|
+
) -> None:
|
|
319
|
+
logger.debug("Running cleanup job for EKS AWS Role and User relationships")
|
|
320
|
+
|
|
321
|
+
cleanup_job = GraphJob.from_node_schema(
|
|
322
|
+
KubernetesUserSchema(), common_job_parameters
|
|
323
|
+
)
|
|
324
|
+
cleanup_job.run(neo4j_session)
|
|
325
|
+
|
|
326
|
+
cleanup_job = GraphJob.from_node_schema(
|
|
327
|
+
KubernetesGroupSchema(), common_job_parameters
|
|
328
|
+
)
|
|
329
|
+
cleanup_job.run(neo4j_session)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def sync(
|
|
333
|
+
neo4j_session: neo4j.Session,
|
|
334
|
+
k8s_client: K8sClient,
|
|
335
|
+
boto3_session: boto3.session.Session,
|
|
336
|
+
region: str,
|
|
337
|
+
update_tag: int,
|
|
338
|
+
cluster_id: str,
|
|
339
|
+
cluster_name: str,
|
|
340
|
+
) -> None:
|
|
341
|
+
"""
|
|
342
|
+
Sync EKS identity providers:
|
|
343
|
+
1. AWS IAM role and user mappings (aws-auth ConfigMap)
|
|
344
|
+
2. External OIDC providers (EKS API)
|
|
345
|
+
"""
|
|
346
|
+
logger.info(f"Starting EKS identity provider sync for cluster {cluster_name}")
|
|
347
|
+
|
|
348
|
+
# 1. Sync AWS IAM mappings (aws-auth ConfigMap)
|
|
349
|
+
logger.info("Syncing AWS IAM mappings from aws-auth ConfigMap")
|
|
350
|
+
configmap = get_aws_auth_configmap(k8s_client)
|
|
351
|
+
auth_mappings = parse_aws_auth_map(configmap)
|
|
352
|
+
|
|
353
|
+
# Transform and load both role and user mappings
|
|
354
|
+
if auth_mappings.get("roles") or auth_mappings.get("users"):
|
|
355
|
+
transformed_data = transform_aws_auth_mappings(auth_mappings, cluster_name)
|
|
356
|
+
load_aws_auth_mappings(
|
|
357
|
+
neo4j_session,
|
|
358
|
+
transformed_data["users"],
|
|
359
|
+
transformed_data["groups"],
|
|
360
|
+
update_tag,
|
|
361
|
+
cluster_id,
|
|
362
|
+
cluster_name,
|
|
363
|
+
)
|
|
364
|
+
logger.info(
|
|
365
|
+
f"Successfully synced {len(auth_mappings.get('roles', []))} AWS IAM role mappings "
|
|
366
|
+
f"and {len(auth_mappings.get('users', []))} AWS IAM user mappings"
|
|
367
|
+
)
|
|
368
|
+
else:
|
|
369
|
+
logger.info("No role or user mappings found in aws-auth ConfigMap")
|
|
370
|
+
|
|
371
|
+
# 2. Sync External OIDC providers (EKS API)
|
|
372
|
+
logger.info("Syncing external OIDC providers from EKS API")
|
|
373
|
+
|
|
374
|
+
# Get OIDC providers from EKS API
|
|
375
|
+
oidc_provider = get_oidc_provider(boto3_session, region, cluster_name)
|
|
376
|
+
|
|
377
|
+
if oidc_provider:
|
|
378
|
+
# Transform OIDC providers (infrastructure metadata only)
|
|
379
|
+
transformed_oidc_provider = transform_oidc_provider(oidc_provider, cluster_name)
|
|
380
|
+
|
|
381
|
+
# Load OIDC providers
|
|
382
|
+
load_oidc_provider(
|
|
383
|
+
neo4j_session,
|
|
384
|
+
transformed_oidc_provider,
|
|
385
|
+
update_tag,
|
|
386
|
+
cluster_id,
|
|
387
|
+
cluster_name,
|
|
388
|
+
)
|
|
389
|
+
logger.info(f"Successfully synced {len(oidc_provider)} external OIDC provider")
|
|
390
|
+
else:
|
|
391
|
+
logger.info("No external OIDC provider found for cluster")
|
|
392
|
+
|
|
393
|
+
# Cleanup
|
|
394
|
+
common_job_parameters = {
|
|
395
|
+
"UPDATE_TAG": update_tag,
|
|
396
|
+
"CLUSTER_ID": cluster_id,
|
|
397
|
+
}
|
|
398
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
399
|
+
|
|
400
|
+
logger.info(
|
|
401
|
+
f"Successfully completed EKS identity provider sync for cluster {cluster_name}"
|
|
402
|
+
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from itertools import chain
|
|
2
3
|
from typing import Any
|
|
3
4
|
from typing import Dict
|
|
4
5
|
from typing import List
|
|
@@ -19,9 +20,11 @@ from cartography.models.kubernetes.clusterrolebindings import (
|
|
|
19
20
|
KubernetesClusterRoleBindingSchema,
|
|
20
21
|
)
|
|
21
22
|
from cartography.models.kubernetes.clusterroles import KubernetesClusterRoleSchema
|
|
23
|
+
from cartography.models.kubernetes.groups import KubernetesGroupSchema
|
|
22
24
|
from cartography.models.kubernetes.rolebindings import KubernetesRoleBindingSchema
|
|
23
25
|
from cartography.models.kubernetes.roles import KubernetesRoleSchema
|
|
24
26
|
from cartography.models.kubernetes.serviceaccounts import KubernetesServiceAccountSchema
|
|
27
|
+
from cartography.models.kubernetes.users import KubernetesUserSchema
|
|
25
28
|
from cartography.util import timeit
|
|
26
29
|
|
|
27
30
|
logger = logging.getLogger(__name__)
|
|
@@ -263,6 +266,62 @@ def transform_cluster_role_bindings(
|
|
|
263
266
|
return result
|
|
264
267
|
|
|
265
268
|
|
|
269
|
+
def transform_users(
|
|
270
|
+
role_bindings: List[V1RoleBinding],
|
|
271
|
+
cluster_role_bindings: List[V1ClusterRoleBinding],
|
|
272
|
+
cluster_name: str,
|
|
273
|
+
) -> List[Dict[str, Any]]:
|
|
274
|
+
"""
|
|
275
|
+
Transform Kubernetes Users from RoleBindings and ClusterRoleBindings into a list of dicts.
|
|
276
|
+
"""
|
|
277
|
+
# Extract all users from rolebindings and clusterrolebindings
|
|
278
|
+
all_users = {
|
|
279
|
+
subject.name
|
|
280
|
+
for binding in chain(
|
|
281
|
+
role_bindings, cluster_role_bindings
|
|
282
|
+
) # iterate through combined bindings and role bindings
|
|
283
|
+
for subject in (
|
|
284
|
+
binding.subjects or []
|
|
285
|
+
) # iterates through each binding's subjects to get unique users
|
|
286
|
+
if subject.kind == "User"
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return [
|
|
290
|
+
{
|
|
291
|
+
"id": f"{cluster_name}/{user_name}",
|
|
292
|
+
"name": user_name,
|
|
293
|
+
"cluster_name": cluster_name,
|
|
294
|
+
}
|
|
295
|
+
for user_name in sorted(all_users)
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def transform_groups(
|
|
300
|
+
role_bindings: List[V1RoleBinding],
|
|
301
|
+
cluster_role_bindings: List[V1ClusterRoleBinding],
|
|
302
|
+
cluster_name: str,
|
|
303
|
+
) -> List[Dict[str, Any]]:
|
|
304
|
+
"""
|
|
305
|
+
Transform Kubernetes Groups from RoleBindings and ClusterRoleBindings into a list of dicts.
|
|
306
|
+
"""
|
|
307
|
+
# Extract all groups from rolebindings and clusterrolebindings
|
|
308
|
+
all_groups = {
|
|
309
|
+
subject.name
|
|
310
|
+
for binding in chain(role_bindings, cluster_role_bindings)
|
|
311
|
+
for subject in (binding.subjects or [])
|
|
312
|
+
if subject.kind == "Group"
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return [
|
|
316
|
+
{
|
|
317
|
+
"id": f"{cluster_name}/{group_name}",
|
|
318
|
+
"name": group_name,
|
|
319
|
+
"cluster_name": cluster_name,
|
|
320
|
+
}
|
|
321
|
+
for group_name in sorted(all_groups)
|
|
322
|
+
]
|
|
323
|
+
|
|
324
|
+
|
|
266
325
|
@timeit
|
|
267
326
|
def load_service_accounts(
|
|
268
327
|
session: neo4j.Session,
|
|
@@ -358,6 +417,44 @@ def load_cluster_role_bindings(
|
|
|
358
417
|
)
|
|
359
418
|
|
|
360
419
|
|
|
420
|
+
@timeit
|
|
421
|
+
def load_users(
|
|
422
|
+
session: neo4j.Session,
|
|
423
|
+
users: List[Dict[str, Any]],
|
|
424
|
+
update_tag: int,
|
|
425
|
+
cluster_id: str,
|
|
426
|
+
cluster_name: str,
|
|
427
|
+
) -> None:
|
|
428
|
+
logger.info(f"Loading {len(users)} KubernetesUsers")
|
|
429
|
+
load(
|
|
430
|
+
session,
|
|
431
|
+
KubernetesUserSchema(),
|
|
432
|
+
users,
|
|
433
|
+
lastupdated=update_tag,
|
|
434
|
+
CLUSTER_ID=cluster_id,
|
|
435
|
+
CLUSTER_NAME=cluster_name,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@timeit
|
|
440
|
+
def load_groups(
|
|
441
|
+
session: neo4j.Session,
|
|
442
|
+
groups: List[Dict[str, Any]],
|
|
443
|
+
update_tag: int,
|
|
444
|
+
cluster_id: str,
|
|
445
|
+
cluster_name: str,
|
|
446
|
+
) -> None:
|
|
447
|
+
logger.info(f"Loading {len(groups)} KubernetesGroups")
|
|
448
|
+
load(
|
|
449
|
+
session,
|
|
450
|
+
KubernetesGroupSchema(),
|
|
451
|
+
groups,
|
|
452
|
+
lastupdated=update_tag,
|
|
453
|
+
CLUSTER_ID=cluster_id,
|
|
454
|
+
CLUSTER_NAME=cluster_name,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
361
458
|
@timeit
|
|
362
459
|
def cleanup(session: neo4j.Session, common_job_parameters: Dict[str, Any]) -> None:
|
|
363
460
|
logger.debug("Running cleanup job for Kubernetes RBAC resources")
|
|
@@ -386,6 +483,16 @@ def cleanup(session: neo4j.Session, common_job_parameters: Dict[str, Any]) -> No
|
|
|
386
483
|
)
|
|
387
484
|
cleanup_job.run(session)
|
|
388
485
|
|
|
486
|
+
cleanup_job = GraphJob.from_node_schema(
|
|
487
|
+
KubernetesUserSchema(), common_job_parameters
|
|
488
|
+
)
|
|
489
|
+
cleanup_job.run(session)
|
|
490
|
+
|
|
491
|
+
cleanup_job = GraphJob.from_node_schema(
|
|
492
|
+
KubernetesGroupSchema(), common_job_parameters
|
|
493
|
+
)
|
|
494
|
+
cleanup_job.run(session)
|
|
495
|
+
|
|
389
496
|
|
|
390
497
|
@timeit
|
|
391
498
|
def sync_kubernetes_rbac(
|
|
@@ -418,9 +525,35 @@ def sync_kubernetes_rbac(
|
|
|
418
525
|
cluster_role_bindings, client.name
|
|
419
526
|
)
|
|
420
527
|
|
|
528
|
+
# Transform users from all bindings
|
|
529
|
+
transformed_users = transform_users(
|
|
530
|
+
role_bindings, cluster_role_bindings, client.name
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# Transform groups from all bindings
|
|
534
|
+
transformed_groups = transform_groups(
|
|
535
|
+
role_bindings, cluster_role_bindings, client.name
|
|
536
|
+
)
|
|
537
|
+
|
|
421
538
|
cluster_id = common_job_parameters["CLUSTER_ID"]
|
|
422
539
|
cluster_name = client.name
|
|
423
540
|
|
|
541
|
+
load_users(
|
|
542
|
+
session=session,
|
|
543
|
+
users=transformed_users,
|
|
544
|
+
update_tag=update_tag,
|
|
545
|
+
cluster_id=cluster_id,
|
|
546
|
+
cluster_name=cluster_name,
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
load_groups(
|
|
550
|
+
session=session,
|
|
551
|
+
groups=transformed_groups,
|
|
552
|
+
update_tag=update_tag,
|
|
553
|
+
cluster_id=cluster_id,
|
|
554
|
+
cluster_name=cluster_name,
|
|
555
|
+
)
|
|
556
|
+
|
|
424
557
|
load_service_accounts(
|
|
425
558
|
session=session,
|
|
426
559
|
service_accounts=transformed_service_accounts,
|