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
cartography/intel/gcp/storage.py
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from typing import Dict
|
|
3
3
|
from typing import List
|
|
4
|
+
from typing import Tuple
|
|
4
5
|
|
|
5
6
|
import neo4j
|
|
6
7
|
from googleapiclient.discovery import HttpError
|
|
7
8
|
from googleapiclient.discovery import Resource
|
|
8
9
|
|
|
10
|
+
from cartography.client.core.tx import load
|
|
11
|
+
from cartography.graph.job import GraphJob
|
|
9
12
|
from cartography.intel.gcp import compute
|
|
10
|
-
from cartography.
|
|
13
|
+
from cartography.models.gcp.storage.bucket import GCPBucketLabelSchema
|
|
14
|
+
from cartography.models.gcp.storage.bucket import GCPBucketSchema
|
|
11
15
|
from cartography.util import timeit
|
|
12
16
|
|
|
13
17
|
logger = logging.getLogger(__name__)
|
|
@@ -58,165 +62,85 @@ def get_gcp_buckets(storage: Resource, project_id: str) -> Dict:
|
|
|
58
62
|
|
|
59
63
|
|
|
60
64
|
@timeit
|
|
61
|
-
def
|
|
65
|
+
def transform_gcp_buckets_and_labels(bucket_res: Dict) -> Tuple[List[Dict], List[Dict]]:
|
|
62
66
|
"""
|
|
63
|
-
Transform the GCP Storage Bucket response object for Neo4j ingestion
|
|
67
|
+
Transform the GCP Storage Bucket response object for Neo4j ingestion.
|
|
64
68
|
|
|
65
|
-
:
|
|
66
|
-
:
|
|
67
|
-
|
|
68
|
-
:rtype: list
|
|
69
|
-
:return: List of buckets ready for ingestion to Neo4j
|
|
69
|
+
:param bucket_res: The raw GCP bucket response.
|
|
70
|
+
:return: A tuple of (buckets, bucket_labels) ready for ingestion to Neo4j.
|
|
70
71
|
"""
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
buckets: List[Dict] = []
|
|
74
|
+
labels: List[Dict] = []
|
|
73
75
|
for b in bucket_res.get("items", []):
|
|
74
|
-
bucket = {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
76
|
+
bucket = {
|
|
77
|
+
"iam_config_bucket_policy_only": (
|
|
78
|
+
b.get("iamConfiguration", {}).get("bucketPolicyOnly", {}).get("enabled")
|
|
79
|
+
),
|
|
80
|
+
"id": b["id"],
|
|
81
|
+
# Preserve legacy bucket_id field for compatibility
|
|
82
|
+
"bucket_id": b["id"],
|
|
83
|
+
"owner_entity": b.get("owner", {}).get("entity"),
|
|
84
|
+
"owner_entity_id": b.get("owner", {}).get("entityId"),
|
|
85
|
+
"kind": b.get("kind"),
|
|
86
|
+
"location": b.get("location"),
|
|
87
|
+
"location_type": b.get("locationType"),
|
|
88
|
+
"meta_generation": b.get("metageneration"),
|
|
89
|
+
"project_number": b.get("projectNumber"),
|
|
90
|
+
"self_link": b.get("selfLink"),
|
|
91
|
+
"storage_class": b.get("storageClass"),
|
|
92
|
+
"time_created": b.get("timeCreated"),
|
|
93
|
+
"versioning_enabled": b.get("versioning", {}).get("enabled"),
|
|
94
|
+
"retention_period": b.get("retentionPolicy", {}).get("retentionPeriod"),
|
|
95
|
+
"default_kms_key_name": b.get("encryption", {}).get("defaultKmsKeyName"),
|
|
96
|
+
"log_bucket": b.get("logging", {}).get("logBucket"),
|
|
97
|
+
"requester_pays": b.get("billing", {}).get("requesterPays"),
|
|
98
|
+
}
|
|
99
|
+
buckets.append(bucket)
|
|
100
|
+
for key, val in b.get("labels", {}).items():
|
|
101
|
+
labels.append(
|
|
102
|
+
{
|
|
103
|
+
"id": f"GCPBucket_{key}",
|
|
104
|
+
"key": key,
|
|
105
|
+
"value": val,
|
|
106
|
+
"bucket_id": b["id"],
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
return buckets, labels
|
|
107
110
|
|
|
108
111
|
|
|
109
112
|
@timeit
|
|
110
113
|
def load_gcp_buckets(
|
|
111
114
|
neo4j_session: neo4j.Session,
|
|
112
115
|
buckets: List[Dict],
|
|
116
|
+
project_id: str,
|
|
113
117
|
gcp_update_tag: int,
|
|
114
118
|
) -> None:
|
|
115
|
-
"""
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
:type gcp_update_tag: timestamp
|
|
125
|
-
:param gcp_update_tag: The timestamp value to set our new Neo4j nodes with
|
|
126
|
-
|
|
127
|
-
:rtype: NoneType
|
|
128
|
-
:return: Nothing
|
|
129
|
-
"""
|
|
130
|
-
|
|
131
|
-
query = """
|
|
132
|
-
MERGE(p:GCPProject{projectnumber:$ProjectNumber})
|
|
133
|
-
ON CREATE SET p.firstseen = timestamp()
|
|
134
|
-
SET p.lastupdated = $gcp_update_tag
|
|
135
|
-
|
|
136
|
-
MERGE(bucket:GCPBucket{id:$BucketId})
|
|
137
|
-
ON CREATE SET bucket.firstseen = timestamp(),
|
|
138
|
-
bucket.bucket_id = $BucketId
|
|
139
|
-
SET bucket.self_link = $SelfLink,
|
|
140
|
-
bucket.project_number = $ProjectNumber,
|
|
141
|
-
bucket.kind = $Kind,
|
|
142
|
-
bucket.location = $Location,
|
|
143
|
-
bucket.location_type = $LocationType,
|
|
144
|
-
bucket.meta_generation = $MetaGeneration,
|
|
145
|
-
bucket.storage_class = $StorageClass,
|
|
146
|
-
bucket.time_created = $TimeCreated,
|
|
147
|
-
bucket.retention_period = $RetentionPeriod,
|
|
148
|
-
bucket.iam_config_bucket_policy_only = $IamConfigBucketPolicyOnly,
|
|
149
|
-
bucket.owner_entity = $OwnerEntity,
|
|
150
|
-
bucket.owner_entity_id = $OwnerEntityId,
|
|
151
|
-
bucket.lastupdated = $gcp_update_tag,
|
|
152
|
-
bucket.versioning_enabled = $VersioningEnabled,
|
|
153
|
-
bucket.log_bucket = $LogBucket,
|
|
154
|
-
bucket.requester_pays = $RequesterPays,
|
|
155
|
-
bucket.default_kms_key_name = $DefaultKmsKeyName
|
|
156
|
-
|
|
157
|
-
MERGE (p)-[r:RESOURCE]->(bucket)
|
|
158
|
-
ON CREATE SET r.firstseen = timestamp()
|
|
159
|
-
SET r.lastupdated = $gcp_update_tag
|
|
160
|
-
"""
|
|
161
|
-
for bucket in buckets:
|
|
162
|
-
neo4j_session.run(
|
|
163
|
-
query,
|
|
164
|
-
ProjectNumber=bucket["project_number"],
|
|
165
|
-
BucketId=bucket["id"],
|
|
166
|
-
SelfLink=bucket["self_link"],
|
|
167
|
-
Kind=bucket["kind"],
|
|
168
|
-
Location=bucket["location"],
|
|
169
|
-
LocationType=bucket["location_type"],
|
|
170
|
-
MetaGeneration=bucket["meta_generation"],
|
|
171
|
-
StorageClass=bucket["storage_class"],
|
|
172
|
-
TimeCreated=bucket["time_created"],
|
|
173
|
-
RetentionPeriod=bucket["retention_period"],
|
|
174
|
-
IamConfigBucketPolicyOnly=bucket["iam_config_bucket_policy_only"],
|
|
175
|
-
OwnerEntity=bucket["owner_entity"],
|
|
176
|
-
OwnerEntityId=bucket["owner_entity_id"],
|
|
177
|
-
VersioningEnabled=bucket["versioning_enabled"],
|
|
178
|
-
LogBucket=bucket["log_bucket"],
|
|
179
|
-
RequesterPays=bucket["requester_pays"],
|
|
180
|
-
DefaultKmsKeyName=bucket["default_kms_key_name"],
|
|
181
|
-
gcp_update_tag=gcp_update_tag,
|
|
182
|
-
)
|
|
183
|
-
_attach_gcp_bucket_labels(neo4j_session, bucket, gcp_update_tag)
|
|
119
|
+
"""Ingest GCP Storage Buckets to Neo4j."""
|
|
120
|
+
load(
|
|
121
|
+
neo4j_session,
|
|
122
|
+
GCPBucketSchema(),
|
|
123
|
+
buckets,
|
|
124
|
+
lastupdated=gcp_update_tag,
|
|
125
|
+
PROJECT_ID=project_id,
|
|
126
|
+
)
|
|
184
127
|
|
|
185
128
|
|
|
186
129
|
@timeit
|
|
187
|
-
def
|
|
130
|
+
def load_gcp_bucket_labels(
|
|
188
131
|
neo4j_session: neo4j.Session,
|
|
189
|
-
|
|
132
|
+
bucket_labels: List[Dict],
|
|
133
|
+
project_id: str,
|
|
190
134
|
gcp_update_tag: int,
|
|
191
135
|
) -> None:
|
|
192
|
-
"""
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
MERGE (l:Label:GCPBucketLabel{id: $BucketLabelId})
|
|
201
|
-
ON CREATE SET l.firstseen = timestamp(),
|
|
202
|
-
l.key = $Key
|
|
203
|
-
SET l.value = $Value,
|
|
204
|
-
l.lastupdated = $gcp_update_tag
|
|
205
|
-
WITH l
|
|
206
|
-
MATCH (bucket:GCPBucket{id:$BucketId})
|
|
207
|
-
MERGE (l)<-[r:LABELED]-(bucket)
|
|
208
|
-
ON CREATE SET r.firstseen = timestamp()
|
|
209
|
-
SET r.lastupdated = $gcp_update_tag
|
|
210
|
-
"""
|
|
211
|
-
for key, val in bucket.get("labels", []):
|
|
212
|
-
neo4j_session.run(
|
|
213
|
-
query,
|
|
214
|
-
BucketLabelId=f"GCPBucket_{key}",
|
|
215
|
-
Key=key,
|
|
216
|
-
Value=val,
|
|
217
|
-
BucketId=bucket["id"],
|
|
218
|
-
gcp_update_tag=gcp_update_tag,
|
|
219
|
-
)
|
|
136
|
+
"""Ingest GCP Storage Bucket labels and attach them to buckets."""
|
|
137
|
+
load(
|
|
138
|
+
neo4j_session,
|
|
139
|
+
GCPBucketLabelSchema(),
|
|
140
|
+
bucket_labels,
|
|
141
|
+
lastupdated=gcp_update_tag,
|
|
142
|
+
PROJECT_ID=project_id,
|
|
143
|
+
)
|
|
220
144
|
|
|
221
145
|
|
|
222
146
|
@timeit
|
|
@@ -224,22 +148,14 @@ def cleanup_gcp_buckets(
|
|
|
224
148
|
neo4j_session: neo4j.Session,
|
|
225
149
|
common_job_parameters: Dict,
|
|
226
150
|
) -> None:
|
|
227
|
-
"""
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
:param common_job_parameters: Dictionary of other job parameters to pass to Neo4j
|
|
235
|
-
|
|
236
|
-
:rtype: NoneType
|
|
237
|
-
:return: Nothing
|
|
238
|
-
"""
|
|
239
|
-
run_cleanup_job(
|
|
240
|
-
"gcp_storage_bucket_cleanup.json",
|
|
151
|
+
"""Delete out-of-date GCP Storage Bucket nodes and relationships."""
|
|
152
|
+
# Bucket labels depend on buckets, so we must remove labels first to avoid
|
|
153
|
+
# dangling references before deleting the buckets themselves.
|
|
154
|
+
GraphJob.from_node_schema(GCPBucketLabelSchema(), common_job_parameters).run(
|
|
155
|
+
neo4j_session,
|
|
156
|
+
)
|
|
157
|
+
GraphJob.from_node_schema(GCPBucketSchema(), common_job_parameters).run(
|
|
241
158
|
neo4j_session,
|
|
242
|
-
common_job_parameters,
|
|
243
159
|
)
|
|
244
160
|
|
|
245
161
|
|
|
@@ -274,7 +190,7 @@ def sync_gcp_buckets(
|
|
|
274
190
|
"""
|
|
275
191
|
logger.info("Syncing Storage objects for project %s.", project_id)
|
|
276
192
|
storage_res = get_gcp_buckets(storage, project_id)
|
|
277
|
-
|
|
278
|
-
load_gcp_buckets(neo4j_session,
|
|
279
|
-
|
|
193
|
+
buckets, bucket_labels = transform_gcp_buckets_and_labels(storage_res)
|
|
194
|
+
load_gcp_buckets(neo4j_session, buckets, project_id, gcp_update_tag)
|
|
195
|
+
load_gcp_bucket_labels(neo4j_session, bucket_labels, project_id, gcp_update_tag)
|
|
280
196
|
cleanup_gcp_buckets(neo4j_session, common_job_parameters)
|
|
@@ -41,12 +41,12 @@ UserAffiliationAndRepoPermission = namedtuple(
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
GITHUB_ORG_REPOS_PAGINATED_GRAPHQL = """
|
|
44
|
-
query($login: String!, $cursor: String) {
|
|
44
|
+
query($login: String!, $cursor: String, $count: Int!) {
|
|
45
45
|
organization(login: $login)
|
|
46
46
|
{
|
|
47
47
|
url
|
|
48
48
|
login
|
|
49
|
-
repositories(first:
|
|
49
|
+
repositories(first: $count, after: $cursor){
|
|
50
50
|
pageInfo{
|
|
51
51
|
endCursor
|
|
52
52
|
hasNextPage
|
|
@@ -168,14 +168,22 @@ def _get_repo_collaborators_inner_func(
|
|
|
168
168
|
repo_name = repo["name"]
|
|
169
169
|
repo_url = repo["url"]
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
171
|
+
# Guard against None when collaborator fields are not accessible due to permissions.
|
|
172
|
+
direct_info = repo.get("directCollaborators")
|
|
173
|
+
outside_info = repo.get("outsideCollaborators")
|
|
174
|
+
|
|
175
|
+
if affiliation == "OUTSIDE":
|
|
176
|
+
total_outside = 0 if not outside_info else outside_info.get("totalCount", 0)
|
|
177
|
+
if total_outside == 0:
|
|
178
|
+
# No outside collaborators or not permitted to view; skip API calls for this repo.
|
|
179
|
+
result[repo_url] = []
|
|
180
|
+
continue
|
|
181
|
+
else: # DIRECT
|
|
182
|
+
total_direct = 0 if not direct_info else direct_info.get("totalCount", 0)
|
|
183
|
+
if total_direct == 0:
|
|
184
|
+
# No direct collaborators or not permitted to view; skip API calls for this repo.
|
|
185
|
+
result[repo_url] = []
|
|
186
|
+
continue
|
|
179
187
|
|
|
180
188
|
logger.info(f"Loading {affiliation} collaborators for repo {repo_name}.")
|
|
181
189
|
collaborators = _get_repo_collaborators(
|
|
@@ -290,6 +298,7 @@ def get(token: str, api_url: str, organization: str) -> List[Dict]:
|
|
|
290
298
|
organization,
|
|
291
299
|
GITHUB_ORG_REPOS_PAGINATED_GRAPHQL,
|
|
292
300
|
"repositories",
|
|
301
|
+
count=50,
|
|
293
302
|
)
|
|
294
303
|
return repos.nodes
|
|
295
304
|
|
cartography/intel/github/util.py
CHANGED
|
@@ -157,6 +157,18 @@ def fetch_all(
|
|
|
157
157
|
retry += 1
|
|
158
158
|
exc = err
|
|
159
159
|
except requests.exceptions.HTTPError as err:
|
|
160
|
+
if (
|
|
161
|
+
err.response is not None
|
|
162
|
+
and err.response.status_code == 502
|
|
163
|
+
and kwargs.get("count")
|
|
164
|
+
and kwargs["count"] > 1
|
|
165
|
+
):
|
|
166
|
+
kwargs["count"] = max(1, kwargs["count"] // 2)
|
|
167
|
+
logger.warning(
|
|
168
|
+
"GitHub: Received 502 response. Reducing page size to %s and retrying.",
|
|
169
|
+
kwargs["count"],
|
|
170
|
+
)
|
|
171
|
+
continue
|
|
160
172
|
retry += 1
|
|
161
173
|
exc = err
|
|
162
174
|
except requests.exceptions.ChunkedEncodingError as err:
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import neo4j
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
import cartography.intel.keycloak.authenticationexecutions
|
|
7
|
+
import cartography.intel.keycloak.authenticationflows
|
|
8
|
+
import cartography.intel.keycloak.clients
|
|
9
|
+
import cartography.intel.keycloak.groups
|
|
10
|
+
import cartography.intel.keycloak.identityproviders
|
|
11
|
+
import cartography.intel.keycloak.organizations
|
|
12
|
+
import cartography.intel.keycloak.realms
|
|
13
|
+
import cartography.intel.keycloak.roles
|
|
14
|
+
import cartography.intel.keycloak.scopes
|
|
15
|
+
import cartography.intel.keycloak.users
|
|
16
|
+
from cartography.config import Config
|
|
17
|
+
from cartography.util import timeit
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
_TIMEOUT = (60, 60)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@timeit
|
|
24
|
+
def start_keycloak_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
|
|
25
|
+
"""
|
|
26
|
+
If this module is configured, perform ingestion of Keycloak data. Otherwise warn and exit
|
|
27
|
+
:param neo4j_session: Neo4J session for database interface
|
|
28
|
+
:param config: A cartography.config object
|
|
29
|
+
:return: None
|
|
30
|
+
"""
|
|
31
|
+
if (
|
|
32
|
+
not config.keycloak_client_id
|
|
33
|
+
or not config.keycloak_client_secret
|
|
34
|
+
or not config.keycloak_url
|
|
35
|
+
or not config.keycloak_realm
|
|
36
|
+
):
|
|
37
|
+
logger.info(
|
|
38
|
+
"Keycloak import is not configured - skipping this module. "
|
|
39
|
+
"See docs to configure.",
|
|
40
|
+
)
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
# Create requests sessions
|
|
44
|
+
with requests.session() as api_session:
|
|
45
|
+
payload = {
|
|
46
|
+
"grant_type": "client_credentials",
|
|
47
|
+
"client_id": config.keycloak_client_id,
|
|
48
|
+
"client_secret": config.keycloak_client_secret,
|
|
49
|
+
}
|
|
50
|
+
req = api_session.post(
|
|
51
|
+
f"{config.keycloak_url}/realms/{config.keycloak_realm}/protocol/openid-connect/token",
|
|
52
|
+
data=payload,
|
|
53
|
+
timeout=_TIMEOUT,
|
|
54
|
+
)
|
|
55
|
+
req.raise_for_status()
|
|
56
|
+
api_session.headers.update(
|
|
57
|
+
{"Authorization": f'Bearer {req.json()["access_token"]}'}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
common_job_parameters = {
|
|
61
|
+
"UPDATE_TAG": config.update_tag,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for realm in cartography.intel.keycloak.realms.sync(
|
|
65
|
+
neo4j_session, api_session, config.keycloak_url, common_job_parameters
|
|
66
|
+
):
|
|
67
|
+
realm_scopped_job_parameters = {
|
|
68
|
+
"UPDATE_TAG": config.update_tag,
|
|
69
|
+
"REALM": realm["realm"],
|
|
70
|
+
"REALM_ID": realm["id"],
|
|
71
|
+
}
|
|
72
|
+
cartography.intel.keycloak.users.sync(
|
|
73
|
+
neo4j_session,
|
|
74
|
+
api_session,
|
|
75
|
+
config.keycloak_url,
|
|
76
|
+
realm_scopped_job_parameters,
|
|
77
|
+
)
|
|
78
|
+
cartography.intel.keycloak.identityproviders.sync(
|
|
79
|
+
neo4j_session,
|
|
80
|
+
api_session,
|
|
81
|
+
config.keycloak_url,
|
|
82
|
+
realm_scopped_job_parameters,
|
|
83
|
+
)
|
|
84
|
+
scopes = cartography.intel.keycloak.scopes.sync(
|
|
85
|
+
neo4j_session,
|
|
86
|
+
api_session,
|
|
87
|
+
config.keycloak_url,
|
|
88
|
+
realm_scopped_job_parameters,
|
|
89
|
+
)
|
|
90
|
+
scope_ids = [s["id"] for s in scopes]
|
|
91
|
+
flows = cartography.intel.keycloak.authenticationflows.sync(
|
|
92
|
+
neo4j_session,
|
|
93
|
+
api_session,
|
|
94
|
+
config.keycloak_url,
|
|
95
|
+
realm_scopped_job_parameters,
|
|
96
|
+
)
|
|
97
|
+
flow_aliases_to_id = {f["alias"]: f["id"] for f in flows}
|
|
98
|
+
cartography.intel.keycloak.authenticationexecutions.sync(
|
|
99
|
+
neo4j_session,
|
|
100
|
+
api_session,
|
|
101
|
+
config.keycloak_url,
|
|
102
|
+
realm_scopped_job_parameters,
|
|
103
|
+
list(flow_aliases_to_id.keys()),
|
|
104
|
+
)
|
|
105
|
+
realm_default_flows = {
|
|
106
|
+
"browser": flow_aliases_to_id.get(realm.get("browserFlow")),
|
|
107
|
+
"registration": flow_aliases_to_id.get(realm.get("registrationFlow")),
|
|
108
|
+
"direct_grant": flow_aliases_to_id.get(realm.get("directGrantFlow")),
|
|
109
|
+
"reset_credentials": flow_aliases_to_id.get(
|
|
110
|
+
realm.get("resetCredentialsFlow")
|
|
111
|
+
),
|
|
112
|
+
"client_authentication": flow_aliases_to_id.get(
|
|
113
|
+
realm.get("clientAuthenticationFlow")
|
|
114
|
+
),
|
|
115
|
+
"docker_authentication": flow_aliases_to_id.get(
|
|
116
|
+
realm.get("dockerAuthenticationFlow")
|
|
117
|
+
),
|
|
118
|
+
"first_broker_login": flow_aliases_to_id.get(
|
|
119
|
+
realm.get("firstBrokerLoginFlow")
|
|
120
|
+
),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
clients = cartography.intel.keycloak.clients.sync(
|
|
124
|
+
neo4j_session,
|
|
125
|
+
api_session,
|
|
126
|
+
config.keycloak_url,
|
|
127
|
+
realm_scopped_job_parameters,
|
|
128
|
+
realm_default_flows,
|
|
129
|
+
)
|
|
130
|
+
client_ids = [c["id"] for c in clients]
|
|
131
|
+
cartography.intel.keycloak.roles.sync(
|
|
132
|
+
neo4j_session,
|
|
133
|
+
api_session,
|
|
134
|
+
config.keycloak_url,
|
|
135
|
+
realm_scopped_job_parameters,
|
|
136
|
+
client_ids,
|
|
137
|
+
scope_ids,
|
|
138
|
+
)
|
|
139
|
+
cartography.intel.keycloak.groups.sync(
|
|
140
|
+
neo4j_session,
|
|
141
|
+
api_session,
|
|
142
|
+
config.keycloak_url,
|
|
143
|
+
realm_scopped_job_parameters,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Organizations if they are enabled
|
|
147
|
+
if realm.get("organizationsEnabled", False):
|
|
148
|
+
cartography.intel.keycloak.organizations.sync(
|
|
149
|
+
neo4j_session,
|
|
150
|
+
api_session,
|
|
151
|
+
config.keycloak_url,
|
|
152
|
+
realm_scopped_job_parameters,
|
|
153
|
+
)
|