cartography 0.110.0rc1__py3-none-any.whl → 0.111.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 (87) hide show
  1. cartography/_version.py +16 -3
  2. cartography/cli.py +46 -8
  3. cartography/config.py +16 -9
  4. cartography/data/indexes.cypher +0 -2
  5. cartography/data/jobs/analysis/aws_ec2_keypair_analysis.json +2 -2
  6. cartography/data/jobs/analysis/keycloak_inheritance.json +30 -0
  7. cartography/graph/querybuilder.py +70 -0
  8. cartography/intel/aws/apigateway.py +113 -4
  9. cartography/intel/aws/cognito.py +201 -0
  10. cartography/intel/aws/ec2/vpc.py +140 -124
  11. cartography/intel/aws/ecs.py +7 -1
  12. cartography/intel/aws/eventbridge.py +73 -0
  13. cartography/intel/aws/glue.py +64 -0
  14. cartography/intel/aws/kms.py +13 -1
  15. cartography/intel/aws/rds.py +105 -0
  16. cartography/intel/aws/resources.py +2 -0
  17. cartography/intel/aws/route53.py +3 -1
  18. cartography/intel/aws/s3.py +104 -0
  19. cartography/intel/entra/__init__.py +41 -43
  20. cartography/intel/entra/applications.py +2 -1
  21. cartography/intel/entra/ou.py +1 -1
  22. cartography/intel/github/__init__.py +21 -25
  23. cartography/intel/github/repos.py +32 -48
  24. cartography/intel/github/util.py +12 -0
  25. cartography/intel/keycloak/__init__.py +153 -0
  26. cartography/intel/keycloak/authenticationexecutions.py +322 -0
  27. cartography/intel/keycloak/authenticationflows.py +77 -0
  28. cartography/intel/keycloak/clients.py +187 -0
  29. cartography/intel/keycloak/groups.py +126 -0
  30. cartography/intel/keycloak/identityproviders.py +94 -0
  31. cartography/intel/keycloak/organizations.py +163 -0
  32. cartography/intel/keycloak/realms.py +61 -0
  33. cartography/intel/keycloak/roles.py +202 -0
  34. cartography/intel/keycloak/scopes.py +73 -0
  35. cartography/intel/keycloak/users.py +70 -0
  36. cartography/intel/keycloak/util.py +47 -0
  37. cartography/intel/kubernetes/__init__.py +4 -0
  38. cartography/intel/kubernetes/rbac.py +464 -0
  39. cartography/intel/kubernetes/util.py +17 -0
  40. cartography/models/aws/apigateway/apigatewaydeployment.py +74 -0
  41. cartography/models/aws/cognito/__init__.py +0 -0
  42. cartography/models/aws/cognito/identity_pool.py +70 -0
  43. cartography/models/aws/cognito/user_pool.py +47 -0
  44. cartography/models/aws/ec2/security_groups.py +1 -1
  45. cartography/models/aws/ec2/vpc.py +46 -0
  46. cartography/models/aws/ec2/vpc_cidr.py +102 -0
  47. cartography/models/aws/ecs/services.py +17 -0
  48. cartography/models/aws/ecs/tasks.py +1 -0
  49. cartography/models/aws/eventbridge/target.py +71 -0
  50. cartography/models/aws/glue/job.py +69 -0
  51. cartography/models/aws/rds/event_subscription.py +146 -0
  52. cartography/models/aws/route53/dnsrecord.py +21 -0
  53. cartography/models/github/dependencies.py +1 -2
  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 +98 -0
  67. cartography/models/kubernetes/clusterroles.py +52 -0
  68. cartography/models/kubernetes/rolebindings.py +119 -0
  69. cartography/models/kubernetes/roles.py +76 -0
  70. cartography/models/kubernetes/serviceaccounts.py +77 -0
  71. cartography/models/tailscale/device.py +1 -0
  72. cartography/sync.py +2 -0
  73. cartography/util.py +8 -0
  74. {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/METADATA +4 -3
  75. {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/RECORD +85 -46
  76. cartography/data/jobs/cleanup/aws_import_vpc_cleanup.json +0 -23
  77. cartography/intel/entra/resources.py +0 -20
  78. /cartography/data/jobs/{analysis → scoped_analysis}/aws_s3acl_analysis.json +0 -0
  79. /cartography/models/aws/{__init__.py → apigateway/__init__.py} +0 -0
  80. /cartography/models/aws/{apigateway.py → apigateway/apigateway.py} +0 -0
  81. /cartography/models/aws/{apigatewaycertificate.py → apigateway/apigatewaycertificate.py} +0 -0
  82. /cartography/models/aws/{apigatewayresource.py → apigateway/apigatewayresource.py} +0 -0
  83. /cartography/models/aws/{apigatewaystage.py → apigateway/apigatewaystage.py} +0 -0
  84. {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/WHEEL +0 -0
  85. {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/entry_points.txt +0 -0
  86. {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/licenses/LICENSE +0 -0
  87. {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,202 @@
1
+ import logging
2
+ from typing import Any
3
+ from urllib.parse import quote
4
+
5
+ import neo4j
6
+ import requests
7
+
8
+ from cartography.client.core.tx import load
9
+ from cartography.graph.job import GraphJob
10
+ from cartography.intel.keycloak.util import get_paginated
11
+ from cartography.models.keycloak.role import KeycloakRoleSchema
12
+ from cartography.util import timeit
13
+
14
+ logger = logging.getLogger(__name__)
15
+ # Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
16
+ _TIMEOUT = (60, 60)
17
+
18
+
19
+ @timeit
20
+ def sync(
21
+ neo4j_session: neo4j.Session,
22
+ api_session: requests.Session,
23
+ base_url: str,
24
+ common_job_parameters: dict[str, Any],
25
+ client_ids: list[str],
26
+ scope_ids: list[str],
27
+ ) -> None:
28
+ roles = get(api_session, base_url, common_job_parameters["REALM"], client_ids)
29
+ scope_role_mapping = get_mapping(
30
+ api_session,
31
+ base_url,
32
+ common_job_parameters["REALM"],
33
+ scope_ids,
34
+ )
35
+ transformed_roles = transform(roles, scope_role_mapping)
36
+ load_roles(
37
+ neo4j_session,
38
+ transformed_roles,
39
+ common_job_parameters["REALM"],
40
+ common_job_parameters["UPDATE_TAG"],
41
+ )
42
+ cleanup(neo4j_session, common_job_parameters)
43
+
44
+
45
+ @timeit
46
+ def get(
47
+ api_session: requests.Session, base_url: str, realm: str, client_ids: list[str]
48
+ ) -> list[dict[str, Any]]:
49
+ roles_by_id: dict[str, dict[str, Any]] = {}
50
+
51
+ # Get roles at the REALM level
52
+ url = f"{base_url}/admin/realms/{realm}/roles"
53
+ for role in get_paginated(api_session, url, params={"briefRepresentation": False}):
54
+ if role.get("composite", False):
55
+ # If the role is composite, we need to get its composites
56
+ composite_roles = get_paginated(
57
+ api_session,
58
+ f"{base_url}/admin/realms/{realm}/roles-by-id/{role['id']}/composites",
59
+ )
60
+ role["_composite_roles"] = [
61
+ composite_role["id"] for composite_role in composite_roles
62
+ ]
63
+ # Get the direct members
64
+ direct_members = get_paginated(
65
+ api_session,
66
+ f"{base_url}/admin/realms/{realm}/roles/{quote(role['name'])}/users",
67
+ )
68
+ role["_direct_members"] = [member["id"] for member in direct_members]
69
+ roles_by_id[role["id"]] = role
70
+
71
+ # Get roles for each client
72
+ for client_id in client_ids:
73
+ url = f"{base_url}/admin/realms/{realm}/clients/{client_id}/roles"
74
+ for role in get_paginated(
75
+ api_session, url, params={"briefRepresentation": False}
76
+ ):
77
+ # If the role is composite, we need to get its composites
78
+ if role.get("composite", False):
79
+ composite_roles = get_paginated(
80
+ api_session,
81
+ f"{base_url}/admin/realms/{realm}/roles-by-id/{role['id']}/composites",
82
+ )
83
+ role["_composite_roles"] = [
84
+ composite_role["id"] for composite_role in composite_roles
85
+ ]
86
+ # Get the direct members
87
+ direct_members = get_paginated(
88
+ api_session,
89
+ f"{base_url}/admin/realms/{realm}/clients/{client_id}/roles/{quote(role['name'])}/users",
90
+ )
91
+ role["_direct_members"] = [member["id"] for member in direct_members]
92
+ roles_by_id[role["id"]] = role
93
+ return list(roles_by_id.values())
94
+
95
+
96
+ @timeit
97
+ def get_mapping(
98
+ api_session: requests.Session,
99
+ base_url: str,
100
+ realm: str,
101
+ scope_ids: list[str],
102
+ ) -> dict[str, Any]:
103
+ result: dict[str, Any] = {}
104
+ for scope_id in scope_ids:
105
+ mappings_url = (
106
+ f"{base_url}/admin/realms/{realm}/client-scopes/{scope_id}/scope-mappings"
107
+ )
108
+ req = api_session.get(
109
+ mappings_url,
110
+ timeout=_TIMEOUT,
111
+ )
112
+ req.raise_for_status()
113
+ result[scope_id] = req.json()
114
+ return result
115
+
116
+
117
+ def transform(
118
+ roles: list[dict[str, Any]], scope_role_mapping: dict[str, Any]
119
+ ) -> list[dict[str, Any]]:
120
+ transformed_roles = []
121
+
122
+ # Transform the mapping of scopes to roles
123
+ scopes_by_roles: dict[str, list[str]] = {}
124
+ for scope_id, mapping in scope_role_mapping.items():
125
+ for client_details in mapping.get("clientMappings", {}).values():
126
+ for client_mapping in client_details.get("mappings", []):
127
+ scopes_by_roles.setdefault(client_mapping["id"], []).append(scope_id)
128
+ for realm_mapping in mapping.get("realmMappings", []):
129
+ scopes_by_roles.setdefault(realm_mapping["id"], []).append(scope_id)
130
+
131
+ for role in roles:
132
+ role["_scope_ids"] = scopes_by_roles.get(role["id"], None)
133
+ transformed_roles.append(role)
134
+
135
+ return _order_dicts_dfs(
136
+ transformed_roles, children_key="_composite_roles", ignore_unknown_children=True
137
+ )
138
+
139
+
140
+ @timeit
141
+ def load_roles(
142
+ neo4j_session: neo4j.Session,
143
+ data: list[dict[str, Any]],
144
+ realm: str,
145
+ update_tag: int,
146
+ ) -> None:
147
+ logger.info("Loading %d Keycloak Roles (%s) into Neo4j.", len(data), realm)
148
+ load(
149
+ neo4j_session,
150
+ KeycloakRoleSchema(),
151
+ data,
152
+ LASTUPDATED=update_tag,
153
+ REALM=realm,
154
+ )
155
+
156
+
157
+ @timeit
158
+ def cleanup(
159
+ neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
160
+ ) -> None:
161
+ GraphJob.from_node_schema(KeycloakRoleSchema(), common_job_parameters).run(
162
+ neo4j_session
163
+ )
164
+
165
+
166
+ def _order_dicts_dfs(
167
+ nodes: list[dict[str, Any]],
168
+ children_key: str = "children",
169
+ ignore_unknown_children: bool = False,
170
+ ) -> list[dict[str, Any]]:
171
+ by_id = {n["id"]: n for n in nodes}
172
+
173
+ WHITE, GRAY, BLACK = 0, 1, 2 # unvisited, in stack, done
174
+ state: dict[str, int] = {}
175
+ ordered: list[dict[str, Any]] = []
176
+
177
+ def visit(node_id: str, path: list[str]) -> None:
178
+ s = state.get(node_id, WHITE)
179
+ if s == GRAY:
180
+ cycle = " -> ".join(path + [node_id])
181
+ raise ValueError(f"Cycle detected: {cycle}")
182
+ if s == BLACK:
183
+ return
184
+
185
+ state[node_id] = GRAY
186
+ node = by_id[node_id]
187
+
188
+ for child_id in node.get(children_key, []):
189
+ if child_id not in by_id:
190
+ if ignore_unknown_children:
191
+ continue
192
+ raise KeyError(f"Unknown child id: {child_id}")
193
+ visit(child_id, path + [node_id])
194
+
195
+ state[node_id] = BLACK
196
+ ordered.append(node) # post-order: children before parent
197
+
198
+ for nid in by_id:
199
+ if state.get(nid, WHITE) == WHITE:
200
+ visit(nid, [])
201
+
202
+ return ordered
@@ -0,0 +1,73 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ import neo4j
5
+ import requests
6
+
7
+ from cartography.client.core.tx import load
8
+ from cartography.graph.job import GraphJob
9
+ from cartography.intel.keycloak.util import get_paginated
10
+ from cartography.models.keycloak.scope import KeycloakScopeSchema
11
+ from cartography.util import timeit
12
+
13
+ logger = logging.getLogger(__name__)
14
+ # Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
15
+ _TIMEOUT = (60, 60)
16
+
17
+
18
+ @timeit
19
+ def sync(
20
+ neo4j_session: neo4j.Session,
21
+ api_session: requests.Session,
22
+ base_url: str,
23
+ common_job_parameters: dict[str, Any],
24
+ ) -> list[dict[str, Any]]:
25
+ scopes = get(
26
+ api_session,
27
+ base_url,
28
+ common_job_parameters["REALM"],
29
+ )
30
+ load_scopes(
31
+ neo4j_session,
32
+ scopes,
33
+ common_job_parameters["REALM"],
34
+ common_job_parameters["UPDATE_TAG"],
35
+ )
36
+ cleanup(neo4j_session, common_job_parameters)
37
+ return scopes
38
+
39
+
40
+ @timeit
41
+ def get(
42
+ api_session: requests.Session,
43
+ base_url: str,
44
+ realm: str,
45
+ ) -> list[dict[str, Any]]:
46
+ url = f"{base_url}/admin/realms/{realm}/client-scopes"
47
+ return list(get_paginated(api_session, url, params={"briefRepresentation": False}))
48
+
49
+
50
+ @timeit
51
+ def load_scopes(
52
+ neo4j_session: neo4j.Session,
53
+ data: list[dict[str, Any]],
54
+ realm: str,
55
+ update_tag: int,
56
+ ) -> None:
57
+ logger.info("Loading %d Keycloak Scopes (%s) into Neo4j.", len(data), realm)
58
+ load(
59
+ neo4j_session,
60
+ KeycloakScopeSchema(),
61
+ data,
62
+ LASTUPDATED=update_tag,
63
+ REALM=realm,
64
+ )
65
+
66
+
67
+ @timeit
68
+ def cleanup(
69
+ neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
70
+ ) -> None:
71
+ GraphJob.from_node_schema(KeycloakScopeSchema(), common_job_parameters).run(
72
+ neo4j_session
73
+ )
@@ -0,0 +1,70 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ import neo4j
5
+ import requests
6
+
7
+ from cartography.client.core.tx import load
8
+ from cartography.graph.job import GraphJob
9
+ from cartography.intel.keycloak.util import get_paginated
10
+ from cartography.models.keycloak.user import KeycloakUserSchema
11
+ from cartography.util import timeit
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @timeit
17
+ def sync(
18
+ neo4j_session: neo4j.Session,
19
+ api_session: requests.Session,
20
+ base_url: str,
21
+ common_job_parameters: dict[str, Any],
22
+ ) -> None:
23
+ users = get(
24
+ api_session,
25
+ base_url,
26
+ common_job_parameters["REALM"],
27
+ )
28
+ load_users(
29
+ neo4j_session,
30
+ users,
31
+ common_job_parameters["REALM"],
32
+ common_job_parameters["UPDATE_TAG"],
33
+ )
34
+ cleanup(neo4j_session, common_job_parameters)
35
+
36
+
37
+ @timeit
38
+ def get(
39
+ api_session: requests.Session,
40
+ base_url: str,
41
+ realm: str,
42
+ ) -> list[dict[str, Any]]:
43
+ url = f"{base_url}/admin/realms/{realm}/users"
44
+ return list(get_paginated(api_session, url))
45
+
46
+
47
+ @timeit
48
+ def load_users(
49
+ neo4j_session: neo4j.Session,
50
+ data: list[dict[str, Any]],
51
+ realm: str,
52
+ update_tag: int,
53
+ ) -> None:
54
+ logger.info("Loading %d Keycloak Users (%s) into Neo4j.", len(data), realm)
55
+ load(
56
+ neo4j_session,
57
+ KeycloakUserSchema(),
58
+ data,
59
+ LASTUPDATED=update_tag,
60
+ REALM=realm,
61
+ )
62
+
63
+
64
+ @timeit
65
+ def cleanup(
66
+ neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
67
+ ) -> None:
68
+ GraphJob.from_node_schema(KeycloakUserSchema(), common_job_parameters).run(
69
+ neo4j_session
70
+ )
@@ -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)
@@ -6,6 +6,7 @@ from cartography.config import Config
6
6
  from cartography.intel.kubernetes.clusters import sync_kubernetes_cluster
7
7
  from cartography.intel.kubernetes.namespaces import sync_namespaces
8
8
  from cartography.intel.kubernetes.pods import sync_pods
9
+ from cartography.intel.kubernetes.rbac import sync_kubernetes_rbac
9
10
  from cartography.intel.kubernetes.secrets import sync_secrets
10
11
  from cartography.intel.kubernetes.services import sync_services
11
12
  from cartography.intel.kubernetes.util import get_k8s_clients
@@ -38,6 +39,9 @@ def start_k8s_ingestion(session: Session, config: Config) -> None:
38
39
  common_job_parameters["CLUSTER_ID"] = cluster_info.get("id")
39
40
 
40
41
  sync_namespaces(session, client, config.update_tag, common_job_parameters)
42
+ sync_kubernetes_rbac(
43
+ session, client, config.update_tag, common_job_parameters
44
+ )
41
45
  all_pods = sync_pods(
42
46
  session,
43
47
  client,